@cedarjs/uploads 6.0.3-next.227

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +24 -0
  2. package/dist/authenticator.d.ts +38 -0
  3. package/dist/authenticator.d.ts.map +1 -0
  4. package/dist/authenticator.js +51 -0
  5. package/dist/cleanupStaleUploads.d.ts +50 -0
  6. package/dist/cleanupStaleUploads.d.ts.map +1 -0
  7. package/dist/cleanupStaleUploads.js +80 -0
  8. package/dist/constants.d.ts +7 -0
  9. package/dist/constants.d.ts.map +1 -0
  10. package/dist/constants.js +4 -0
  11. package/dist/deleteFile.d.ts +14 -0
  12. package/dist/deleteFile.d.ts.map +1 -0
  13. package/dist/deleteFile.js +18 -0
  14. package/dist/directives.d.ts +55 -0
  15. package/dist/directives.d.ts.map +1 -0
  16. package/dist/directives.js +174 -0
  17. package/dist/errors.d.ts +12 -0
  18. package/dist/errors.d.ts.map +1 -0
  19. package/dist/errors.js +33 -0
  20. package/dist/fastify/plugin.d.ts +49 -0
  21. package/dist/fastify/plugin.d.ts.map +1 -0
  22. package/dist/fastify/plugin.js +241 -0
  23. package/dist/index.d.ts +34 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +88 -0
  26. package/dist/keys.d.ts +18 -0
  27. package/dist/keys.d.ts.map +1 -0
  28. package/dist/keys.js +25 -0
  29. package/dist/pending.d.ts +39 -0
  30. package/dist/pending.d.ts.map +1 -0
  31. package/dist/pending.js +94 -0
  32. package/dist/profiles.d.ts +49 -0
  33. package/dist/profiles.d.ts.map +1 -0
  34. package/dist/profiles.js +68 -0
  35. package/dist/providers/db.d.ts +9 -0
  36. package/dist/providers/db.d.ts.map +1 -0
  37. package/dist/providers/db.js +41 -0
  38. package/dist/providers/fs.d.ts +34 -0
  39. package/dist/providers/fs.d.ts.map +1 -0
  40. package/dist/providers/fs.js +125 -0
  41. package/dist/providers/s3.d.ts +25 -0
  42. package/dist/providers/s3.d.ts.map +1 -0
  43. package/dist/providers/s3.js +117 -0
  44. package/dist/s3.d.ts +3 -0
  45. package/dist/s3.d.ts.map +1 -0
  46. package/dist/s3.js +4 -0
  47. package/dist/serialize.d.ts +10 -0
  48. package/dist/serialize.d.ts.map +1 -0
  49. package/dist/serialize.js +22 -0
  50. package/dist/serveToken.d.ts +36 -0
  51. package/dist/serveToken.d.ts.map +1 -0
  52. package/dist/serveToken.js +45 -0
  53. package/dist/services.d.ts +70 -0
  54. package/dist/services.d.ts.map +1 -0
  55. package/dist/services.js +141 -0
  56. package/dist/storeFile.d.ts +27 -0
  57. package/dist/storeFile.d.ts.map +1 -0
  58. package/dist/storeFile.js +53 -0
  59. package/dist/targets.d.ts +14 -0
  60. package/dist/targets.d.ts.map +1 -0
  61. package/dist/targets.js +21 -0
  62. package/dist/types.d.ts +212 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +0 -0
  65. package/dist/uploadLoader.d.ts +11 -0
  66. package/dist/uploadLoader.d.ts.map +1 -0
  67. package/dist/uploadLoader.js +47 -0
  68. package/dist/uploadToken.d.ts +43 -0
  69. package/dist/uploadToken.d.ts.map +1 -0
  70. package/dist/uploadToken.js +61 -0
  71. package/dist/web/components/DbInput.d.ts +18 -0
  72. package/dist/web/components/DbInput.d.ts.map +1 -0
  73. package/dist/web/components/DbInput.js +46 -0
  74. package/dist/web/components/FsUploader.d.ts +6 -0
  75. package/dist/web/components/FsUploader.d.ts.map +1 -0
  76. package/dist/web/components/FsUploader.js +21 -0
  77. package/dist/web/components/S3Uploader.d.ts +6 -0
  78. package/dist/web/components/S3Uploader.d.ts.map +1 -0
  79. package/dist/web/components/S3Uploader.js +21 -0
  80. package/dist/web/components/UppyUploader.d.ts +23 -0
  81. package/dist/web/components/UppyUploader.d.ts.map +1 -0
  82. package/dist/web/components/UppyUploader.js +72 -0
  83. package/dist/web/createUppy.d.ts +62 -0
  84. package/dist/web/createUppy.d.ts.map +1 -0
  85. package/dist/web/createUppy.js +82 -0
  86. package/dist/web/graphql.d.ts +48 -0
  87. package/dist/web/graphql.d.ts.map +1 -0
  88. package/dist/web/graphql.js +36 -0
  89. package/dist/web/hooks/useDbUpload.d.ts +31 -0
  90. package/dist/web/hooks/useDbUpload.d.ts.map +1 -0
  91. package/dist/web/hooks/useDbUpload.js +80 -0
  92. package/dist/web/hooks/useFsUpload.d.ts +20 -0
  93. package/dist/web/hooks/useFsUpload.d.ts.map +1 -0
  94. package/dist/web/hooks/useFsUpload.js +58 -0
  95. package/dist/web/hooks/useS3Upload.d.ts +23 -0
  96. package/dist/web/hooks/useS3Upload.d.ts.map +1 -0
  97. package/dist/web/hooks/useS3Upload.js +99 -0
  98. package/dist/web/hooks/useUploadToken.d.ts +34 -0
  99. package/dist/web/hooks/useUploadToken.d.ts.map +1 -0
  100. package/dist/web/hooks/useUploadToken.js +60 -0
  101. package/dist/web/hooks/useUppyUpload.d.ts +23 -0
  102. package/dist/web/hooks/useUppyUpload.d.ts.map +1 -0
  103. package/dist/web/hooks/useUppyUpload.js +76 -0
  104. package/dist/web/index.d.ts +22 -0
  105. package/dist/web/index.d.ts.map +1 -0
  106. package/dist/web/index.js +36 -0
  107. package/dist/webhooks/s3.d.ts +52 -0
  108. package/dist/webhooks/s3.d.ts.map +1 -0
  109. package/dist/webhooks/s3.js +122 -0
  110. package/dist/webhooks/sns.d.ts +34 -0
  111. package/dist/webhooks/sns.d.ts.map +1 -0
  112. package/dist/webhooks/sns.js +131 -0
  113. package/package.json +147 -0
@@ -0,0 +1,141 @@
1
+ import { UploadError } from "./errors.js";
2
+ import { generateStorageKey } from "./keys.js";
3
+ import {
4
+ assertOwnership,
5
+ assertTokenAllowsFile,
6
+ createPendingUpload
7
+ } from "./pending.js";
8
+ import { resolveProfile } from "./profiles.js";
9
+ import { resolveTarget } from "./targets.js";
10
+ import { createUploadToken } from "./uploadToken.js";
11
+ function issueUploadToken({
12
+ profiles,
13
+ profile: profileName,
14
+ secret,
15
+ currentUser,
16
+ organizationId
17
+ }) {
18
+ if (!currentUser) {
19
+ throw new UploadError(
20
+ "UNAUTHENTICATED",
21
+ "You must be logged in to request an upload token."
22
+ );
23
+ }
24
+ const profile = resolveProfile(profiles, profileName);
25
+ const org = organizationId ?? currentUser.organizationId ?? void 0;
26
+ const token = createUploadToken({
27
+ payload: {
28
+ profile: profile.name,
29
+ allowedMimeTypes: profile.allowedMimeTypes,
30
+ maxFileSize: profile.maxFileSize,
31
+ maxFiles: profile.maxFiles,
32
+ target: profile.target,
33
+ sub: String(currentUser.id),
34
+ ...org ? { organizationId: org } : {}
35
+ },
36
+ secret,
37
+ expiresIn: profile.expiresIn
38
+ });
39
+ return {
40
+ token,
41
+ allowedMimeTypes: profile.allowedMimeTypes,
42
+ maxFileSize: BigInt(profile.maxFileSize),
43
+ maxFiles: profile.maxFiles
44
+ };
45
+ }
46
+ function toBigInt(value, what) {
47
+ try {
48
+ return BigInt(value);
49
+ } catch {
50
+ throw new UploadError("FILE_TOO_LARGE", `Invalid ${what}: ${value}`);
51
+ }
52
+ }
53
+ async function createPresignedUpload({
54
+ db,
55
+ targets,
56
+ tokenPayload,
57
+ input
58
+ }) {
59
+ const size = toBigInt(input.size, "file size");
60
+ assertTokenAllowsFile(tokenPayload, { contentType: input.contentType, size });
61
+ const target = resolveTarget(targets, tokenPayload.target);
62
+ const storageKey = generateStorageKey(input.contentType);
63
+ const upload = await createPendingUpload({
64
+ db,
65
+ payload: tokenPayload,
66
+ filename: input.filename,
67
+ mimeType: input.contentType,
68
+ size,
69
+ storageKey
70
+ });
71
+ let presigned;
72
+ try {
73
+ presigned = await target.getPresignedUploadUrl(storageKey, {
74
+ contentType: input.contentType,
75
+ size
76
+ });
77
+ } catch (e) {
78
+ await db.upload.delete({ where: { id: upload.id } }).catch(() => void 0);
79
+ throw e;
80
+ }
81
+ return { uploadId: upload.id, ...presigned };
82
+ }
83
+ async function confirmUpload({
84
+ db,
85
+ targets,
86
+ uploadId,
87
+ currentUser
88
+ }) {
89
+ const upload = await db.upload.findFirst({
90
+ where: { id: uploadId },
91
+ omit: { data: true }
92
+ });
93
+ if (!upload) {
94
+ throw new UploadError("NOT_FOUND", "Upload not found.");
95
+ }
96
+ assertOwnership(upload, currentUser, "this upload");
97
+ if (upload.status !== "pending" || !upload.storageKey) {
98
+ throw new UploadError("NOT_PENDING", "Upload is not awaiting confirmation.");
99
+ }
100
+ const target = resolveTarget(targets, upload.target);
101
+ if (!await target.exists(upload.storageKey)) {
102
+ throw new UploadError("NOT_IN_STORAGE", "Upload not found in storage.");
103
+ }
104
+ const actualSize = await target.getObjectSize(upload.storageKey);
105
+ if (actualSize === null || BigInt(actualSize) !== upload.size) {
106
+ const { count: count2 } = await db.upload.updateMany({
107
+ where: { id: uploadId, status: "pending" },
108
+ data: { status: "failed" }
109
+ });
110
+ if (count2 === 1) {
111
+ await target.delete(upload.storageKey);
112
+ }
113
+ throw new UploadError(
114
+ "SIZE_MISMATCH",
115
+ `Uploaded object is ${actualSize ?? "of unknown"} bytes but ${upload.size} bytes were authorized.`
116
+ );
117
+ }
118
+ const { count } = await db.upload.updateMany({
119
+ where: { id: uploadId, status: "pending" },
120
+ data: { status: "completed" }
121
+ });
122
+ const confirmed = await db.upload.findFirst({
123
+ where: { id: uploadId },
124
+ omit: { data: true }
125
+ });
126
+ if (!confirmed) {
127
+ throw new UploadError("NOT_FOUND", "Upload not found.");
128
+ }
129
+ if (count === 0 && confirmed.status !== "completed") {
130
+ throw new UploadError(
131
+ "NOT_PENDING",
132
+ "Upload was marked failed before it could be confirmed."
133
+ );
134
+ }
135
+ return confirmed;
136
+ }
137
+ export {
138
+ confirmUpload,
139
+ createPresignedUpload,
140
+ issueUploadToken
141
+ };
@@ -0,0 +1,27 @@
1
+ import type { StorageProvider, UploadDatabase, UploadRecord } from './types.js';
2
+ export interface StoreFileOptions {
3
+ db: UploadDatabase;
4
+ filename: string;
5
+ mimeType: string;
6
+ data: Uint8Array;
7
+ /**
8
+ * Reject `data` larger than this many bytes. Defaults to the 1 MB DB cap
9
+ * for `providerType: 'db'` targets and to unlimited for object storage.
10
+ */
11
+ maxSize?: number;
12
+ /**
13
+ * Owner of the file, when it belongs to a user. Persisted so the ownership
14
+ * checks that guard user uploads apply to server-stored files too.
15
+ */
16
+ userId?: string;
17
+ /** Owning organization, for multi-tenant apps. */
18
+ organizationId?: string;
19
+ }
20
+ /**
21
+ * Stores a file directly on a target and creates its `Upload` row. This is
22
+ * the path for server-generated files (PDFs, exports, thumbnails) and for
23
+ * files that arrive as base64 through GraphQL. It bypasses tokens, the
24
+ * upload routes, and Uppy entirely.
25
+ */
26
+ export declare function storeFile(target: StorageProvider, options: StoreFileOptions): Promise<UploadRecord>;
27
+ //# sourceMappingURL=storeFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storeFile.d.ts","sourceRoot":"","sources":["../src/storeFile.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAK/E,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,cAAc,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,UAAU,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CAyDvB"}
@@ -0,0 +1,53 @@
1
+ import { UploadError } from "./errors.js";
2
+ import { generateStorageKey } from "./keys.js";
3
+ import { DB_MAX_FILE_SIZE } from "./profiles.js";
4
+ const MIME_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
5
+ async function storeFile(target, options) {
6
+ const { db, filename, mimeType, data, maxSize, userId, organizationId } = options;
7
+ if (!MIME_TYPE.test(mimeType)) {
8
+ throw new UploadError(
9
+ "MIME_TYPE_NOT_ALLOWED",
10
+ `'${mimeType}' is not a valid MIME type.`
11
+ );
12
+ }
13
+ const isDbProvider = target.providerType === "db";
14
+ const cap = maxSize ?? (isDbProvider ? DB_MAX_FILE_SIZE : void 0);
15
+ if (cap !== void 0 && data.byteLength > cap) {
16
+ throw new UploadError(
17
+ "FILE_TOO_LARGE",
18
+ `File is ${data.byteLength} bytes, which exceeds the ${cap} byte limit for target '${target.name}'.`
19
+ );
20
+ }
21
+ const key = isDbProvider ? null : generateStorageKey(mimeType);
22
+ if (key) {
23
+ await target.write(key, data, { contentType: mimeType });
24
+ }
25
+ try {
26
+ return await createRow();
27
+ } catch (e) {
28
+ if (key) {
29
+ await target.delete(key).catch(() => void 0);
30
+ }
31
+ throw e;
32
+ }
33
+ function createRow() {
34
+ return db.upload.create({
35
+ data: {
36
+ target: target.name,
37
+ status: "completed",
38
+ filename,
39
+ mimeType,
40
+ size: BigInt(data.byteLength),
41
+ storageKey: key,
42
+ // Copying gives Prisma the plain-ArrayBuffer-backed array its `Bytes`
43
+ // input requires, whatever `data` was backed by
44
+ data: isDbProvider ? new Uint8Array(data) : null,
45
+ userId: userId ?? null,
46
+ organizationId: organizationId ?? null
47
+ }
48
+ });
49
+ }
50
+ }
51
+ export {
52
+ storeFile
53
+ };
@@ -0,0 +1,14 @@
1
+ import type { StorageProvider, StorageTargets } from './types.js';
2
+ /**
3
+ * Names each provider after its key so that `targets.avatars` works for direct
4
+ * access and `targets[upload.target]` works for lookup from a database row.
5
+ * Returns the same object, typed.
6
+ */
7
+ export declare function defineStorageTargets<T extends StorageTargets>(targets: T): T;
8
+ /**
9
+ * Looks a target up by name, throwing an `UploadError` with code
10
+ * `UNKNOWN_TARGET` that lists the configured names when it is missing. Use it
11
+ * when the name comes from an `Upload` row's `target` column.
12
+ */
13
+ export declare function resolveTarget(targets: StorageTargets, name: string): StorageProvider;
14
+ //# sourceMappingURL=targets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../src/targets.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEjE;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,cAAc,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAM5E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,GACX,eAAe,CAcjB"}
@@ -0,0 +1,21 @@
1
+ import { UploadError } from "./errors.js";
2
+ function defineStorageTargets(targets) {
3
+ for (const [name, provider] of Object.entries(targets)) {
4
+ provider.name = name;
5
+ }
6
+ return targets;
7
+ }
8
+ function resolveTarget(targets, name) {
9
+ const target = Object.hasOwn(targets, name) ? targets[name] : void 0;
10
+ if (!target) {
11
+ throw new UploadError(
12
+ "UNKNOWN_TARGET",
13
+ `Unknown storage target '${name}'. Available targets: ${Object.keys(targets).join(", ") || "(none)"}.`
14
+ );
15
+ }
16
+ return target;
17
+ }
18
+ export {
19
+ defineStorageTargets,
20
+ resolveTarget
21
+ };
@@ -0,0 +1,212 @@
1
+ import type { Readable } from 'node:stream';
2
+ /** Lifecycle state of an `Upload` row. */
3
+ export type UploadStatus = 'pending' | 'completed' | 'failed';
4
+ /**
5
+ * How a served file asks the browser to handle it. `attachment` downloads;
6
+ * `inline` renders in place. Serving defaults to `attachment` because the
7
+ * stored MIME type is client-asserted.
8
+ */
9
+ export type ContentDisposition = 'attachment' | 'inline';
10
+ /**
11
+ * One row of the `Upload` table the setup command adds to `schema.prisma`.
12
+ * `data` is optional because most code paths deliberately leave the inline
13
+ * bytes out of the query.
14
+ */
15
+ export interface UploadRecord {
16
+ id: string;
17
+ target: string;
18
+ status: string;
19
+ filename: string;
20
+ mimeType: string;
21
+ size: bigint;
22
+ storageKey: string | null;
23
+ data?: Uint8Array | null;
24
+ userId: string | null;
25
+ tokenId: string | null;
26
+ organizationId: string | null;
27
+ createdAt: Date;
28
+ updatedAt: Date;
29
+ }
30
+ export interface UploadCreateData {
31
+ target: string;
32
+ status: UploadStatus;
33
+ filename: string;
34
+ mimeType: string;
35
+ size: bigint;
36
+ storageKey?: string | null;
37
+ data?: Uint8Array<ArrayBuffer> | null;
38
+ userId?: string | null;
39
+ tokenId?: string | null;
40
+ organizationId?: string | null;
41
+ }
42
+ export interface UploadWhere {
43
+ id?: string | {
44
+ in: string[];
45
+ };
46
+ target?: string;
47
+ status?: string | {
48
+ in: string[];
49
+ };
50
+ storageKey?: string | null | {
51
+ not: null;
52
+ };
53
+ tokenId?: string;
54
+ createdAt?: {
55
+ lt?: Date;
56
+ gte?: Date;
57
+ };
58
+ updatedAt?: {
59
+ lt?: Date;
60
+ gte?: Date;
61
+ };
62
+ }
63
+ export interface UploadUpdateData {
64
+ status?: UploadStatus;
65
+ size?: bigint;
66
+ storageKey?: string | null;
67
+ }
68
+ /**
69
+ * The subset of Prisma's generated `Upload` delegate this package calls. The
70
+ * generated client satisfies it structurally, so apps pass their `db` as-is.
71
+ */
72
+ export interface UploadDelegate {
73
+ create(args: {
74
+ data: UploadCreateData;
75
+ }): Promise<UploadRecord>;
76
+ findUnique(args: {
77
+ where: {
78
+ id: string;
79
+ };
80
+ }): Promise<UploadRecord | null>;
81
+ findUniqueOrThrow(args: {
82
+ where: {
83
+ id: string;
84
+ };
85
+ }): Promise<UploadRecord>;
86
+ findFirst(args: {
87
+ where: UploadWhere;
88
+ omit?: {
89
+ data?: boolean;
90
+ };
91
+ }): Promise<UploadRecord | null>;
92
+ findMany(args: {
93
+ where: UploadWhere;
94
+ omit?: {
95
+ data?: boolean;
96
+ };
97
+ orderBy?: {
98
+ createdAt: 'asc' | 'desc';
99
+ };
100
+ take?: number;
101
+ }): Promise<UploadRecord[]>;
102
+ updateMany(args: {
103
+ where: UploadWhere;
104
+ data: UploadUpdateData;
105
+ }): Promise<{
106
+ count: number;
107
+ }>;
108
+ delete(args: {
109
+ where: {
110
+ id: string;
111
+ };
112
+ }): Promise<UploadRecord>;
113
+ count(args: {
114
+ where: UploadWhere;
115
+ }): Promise<number>;
116
+ }
117
+ /** What a Prisma interactive transaction hands to its callback. */
118
+ export interface UploadTransactionClient {
119
+ upload: UploadDelegate;
120
+ }
121
+ /**
122
+ * The subset of a Prisma client this package needs: the `Upload` delegate and
123
+ * interactive transactions. Any generated client whose schema contains the
124
+ * `Upload` model satisfies it.
125
+ */
126
+ export interface UploadDatabase extends UploadTransactionClient {
127
+ $transaction<T>(fn: (tx: UploadTransactionClient) => Promise<T>, options?: {
128
+ isolationLevel?: 'Serializable';
129
+ }): Promise<T>;
130
+ }
131
+ /**
132
+ * The shape Uppy's S3 plugin (and any other direct-upload client) needs to
133
+ * PUT a file straight into object storage.
134
+ */
135
+ export interface PresignedUploadUrl {
136
+ url: string;
137
+ method: string;
138
+ headers: Record<string, string>;
139
+ }
140
+ /**
141
+ * The contract every storage provider implements with its native SDK. It is
142
+ * the minimum the framework needs to write, read, serve, and delete files;
143
+ * provider-specific features stay on the provider's own client.
144
+ */
145
+ export interface StorageProvider {
146
+ /** Name of this provider instance, assigned by `defineStorageTargets()`. */
147
+ name: string;
148
+ /**
149
+ * `'db'` marks the provider that stores bytes inline in the `Upload` row.
150
+ * Every other value (`'fs'`, `'s3'`, or a third-party string) is treated
151
+ * identically as object storage.
152
+ */
153
+ providerType: 'db' | (string & {});
154
+ /** Write bytes under `key`. */
155
+ write(key: string, data: Uint8Array, opts: {
156
+ contentType: string;
157
+ }): Promise<void>;
158
+ /** Read the whole object into memory. */
159
+ read(key: string): Promise<Buffer>;
160
+ /**
161
+ * Stream the object. Optional; the serve route falls back to `read()` when
162
+ * a provider does not implement it.
163
+ */
164
+ readStream?(key: string): Promise<Readable>;
165
+ /** Delete the object. A missing object is not an error. */
166
+ delete(key: string): Promise<void>;
167
+ /** Whether the object exists. */
168
+ exists(key: string): Promise<boolean>;
169
+ /**
170
+ * Size of the object in bytes, or `null` when the provider cannot know.
171
+ * Completion-time verification treats `null` as a failed check.
172
+ */
173
+ getObjectSize(key: string): Promise<number | null>;
174
+ /**
175
+ * A time-limited URL for reading the object. Defaults to attachment
176
+ * disposition wherever the provider controls response headers.
177
+ */
178
+ getSignedReadUrl(key: string, opts?: {
179
+ expiresIn?: number;
180
+ disposition?: ContentDisposition;
181
+ }): Promise<string>;
182
+ /**
183
+ * A presigned URL for direct client upload. Providers that cannot do this
184
+ * (FS, DB) throw an `UploadError` with code `PRESIGN_NOT_SUPPORTED`.
185
+ */
186
+ getPresignedUploadUrl(key: string, opts: {
187
+ contentType: string;
188
+ size?: bigint;
189
+ expiresIn?: number;
190
+ }): Promise<PresignedUploadUrl>;
191
+ /**
192
+ * Provider-specific configuration for third-party integrations. S3 returns
193
+ * `{ bucket, region, keyPrefix }`, FS returns `{ uploadDir }`, DB `{}`.
194
+ */
195
+ getConfig(): Record<string, unknown>;
196
+ }
197
+ export type StorageTargets = Record<string, StorageProvider>;
198
+ /** A serialized `Upload` row, safe to return from a route or resolver. */
199
+ export interface SerializedUpload {
200
+ id: string;
201
+ target: string;
202
+ status: string;
203
+ filename: string;
204
+ mimeType: string;
205
+ size: string;
206
+ storageKey: string | null;
207
+ userId: string | null;
208
+ organizationId: string | null;
209
+ createdAt: string;
210
+ updatedAt: string;
211
+ }
212
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ import type { UploadDatabase, UploadRecord } from './types.js';
2
+ /**
3
+ * Loads an `Upload` row by id, batching every call made during the same
4
+ * tick for the same `scope` into one query. A transformer directive runs
5
+ * once per field per row, so a list of a hundred rows would otherwise be a
6
+ * hundred queries. Scope on the GraphQL context so the batch and its cache
7
+ * live exactly one request. The batch query leaves out `data`, so inline
8
+ * blobs are never dragged into memory for object-storage rows.
9
+ */
10
+ export declare function loadUpload(scope: object, db: UploadDatabase, id: string): Promise<UploadRecord | null>;
11
+ //# sourceMappingURL=uploadLoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadLoader.d.ts","sourceRoot":"","sources":["../src/uploadLoader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AA4C9D;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,cAAc,EAClB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA4B9B"}
@@ -0,0 +1,47 @@
1
+ const loaders = /* @__PURE__ */ new WeakMap();
2
+ function flush(loader, db) {
3
+ loader.scheduled = false;
4
+ const batch = loader.queue;
5
+ loader.queue = /* @__PURE__ */ new Map();
6
+ const ids = [...batch.keys()];
7
+ db.upload.findMany({ where: { id: { in: ids } }, omit: { data: true } }).then((rows) => {
8
+ const byId = new Map(rows.map((row) => [row.id, row]));
9
+ for (const [id, waiters] of batch) {
10
+ const row = byId.get(id) ?? null;
11
+ for (const waiter of waiters) {
12
+ waiter.resolve(row);
13
+ }
14
+ }
15
+ }).catch((e) => {
16
+ for (const waiters of batch.values()) {
17
+ for (const waiter of waiters) {
18
+ waiter.reject(e);
19
+ }
20
+ }
21
+ });
22
+ }
23
+ function loadUpload(scope, db, id) {
24
+ let loader = loaders.get(scope);
25
+ if (!loader) {
26
+ loader = { cache: /* @__PURE__ */ new Map(), queue: /* @__PURE__ */ new Map(), scheduled: false };
27
+ loaders.set(scope, loader);
28
+ }
29
+ const cached = loader.cache.get(id);
30
+ if (cached) {
31
+ return cached;
32
+ }
33
+ const promise = new Promise((resolve, reject) => {
34
+ const waiters = loader.queue.get(id) ?? [];
35
+ waiters.push({ resolve, reject });
36
+ loader.queue.set(id, waiters);
37
+ });
38
+ loader.cache.set(id, promise);
39
+ if (!loader.scheduled) {
40
+ loader.scheduled = true;
41
+ queueMicrotask(() => flush(loader, db));
42
+ }
43
+ return promise;
44
+ }
45
+ export {
46
+ loadUpload
47
+ };
@@ -0,0 +1,43 @@
1
+ import type { SignedTokenExpiresIn } from '@cedarjs/api';
2
+ /** Purpose claim that separates upload tokens from every other signed token. */
3
+ export declare const UPLOAD_TOKEN_PURPOSE = "cedar-upload";
4
+ export { UPLOAD_TOKEN_HEADER } from './constants.js';
5
+ /**
6
+ * The claims inside an upload token. Constraints come from the server-side
7
+ * profile; `sub` binds the token to the user it was issued to and `jti`
8
+ * lets the server count how many files one token has created.
9
+ */
10
+ export interface UploadTokenPayload {
11
+ /** The profile the token was issued for. */
12
+ profile: string;
13
+ allowedMimeTypes: string[];
14
+ maxFileSize: number;
15
+ maxFiles: number;
16
+ /** The storage target files created with this token land on. */
17
+ target: string;
18
+ /** Id of the user the token was issued to. */
19
+ sub: string;
20
+ /** Unique token id, stamped onto each `Upload` row the token creates. */
21
+ jti: string;
22
+ /** The organization the token was issued under, in multi-tenant apps. */
23
+ organizationId?: string;
24
+ }
25
+ export interface CreateUploadTokenOptions {
26
+ payload: Omit<UploadTokenPayload, 'jti'> & {
27
+ jti?: string;
28
+ };
29
+ secret: string;
30
+ expiresIn?: SignedTokenExpiresIn;
31
+ }
32
+ /** Signs an upload token. A fresh `jti` is generated unless one is passed. */
33
+ export declare function createUploadToken({ payload, secret, expiresIn, }: CreateUploadTokenOptions): string;
34
+ /**
35
+ * Verifies an upload token and returns its claims. Throws an `UploadError`
36
+ * with code `INVALID_TOKEN` for a missing, expired, tampered, or foreign
37
+ * token. The token argument accepts `null` and `undefined` so a header value
38
+ * can be passed straight in.
39
+ */
40
+ export declare function verifyUploadToken(token: string | null | undefined, { secret }: {
41
+ secret: string;
42
+ }): UploadTokenPayload;
43
+ //# sourceMappingURL=uploadToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadToken.d.ts","sourceRoot":"","sources":["../src/uploadToken.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAKxD,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,iBAAiB,CAAA;AAElD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEpD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAA;IACX,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3D,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,oBAAoB,CAAA;CACjC;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,MAAM,EACN,SAA2C,GAC5C,EAAE,wBAAwB,GAAG,MAAM,CAOnC;AAyBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B,kBAAkB,CA2BpB"}
@@ -0,0 +1,61 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ createSignedToken,
4
+ SignedTokenError,
5
+ verifySignedToken
6
+ } from "@cedarjs/api";
7
+ import { UploadError } from "./errors.js";
8
+ import { DEFAULT_UPLOAD_TOKEN_EXPIRES_IN } from "./profiles.js";
9
+ const UPLOAD_TOKEN_PURPOSE = "cedar-upload";
10
+ import { UPLOAD_TOKEN_HEADER } from "./constants.js";
11
+ function createUploadToken({
12
+ payload,
13
+ secret,
14
+ expiresIn = DEFAULT_UPLOAD_TOKEN_EXPIRES_IN
15
+ }) {
16
+ return createSignedToken({
17
+ payload: { ...payload, jti: payload.jti ?? randomUUID() },
18
+ purpose: UPLOAD_TOKEN_PURPOSE,
19
+ expiresIn,
20
+ secret
21
+ });
22
+ }
23
+ function isStringArray(value) {
24
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
25
+ }
26
+ function isUploadTokenPayload(value) {
27
+ if (typeof value !== "object" || value === null) {
28
+ return false;
29
+ }
30
+ const p = value;
31
+ return typeof p.profile === "string" && isStringArray(p.allowedMimeTypes) && typeof p.maxFileSize === "number" && typeof p.maxFiles === "number" && typeof p.target === "string" && typeof p.sub === "string" && typeof p.jti === "string" && (p.organizationId === void 0 || typeof p.organizationId === "string");
32
+ }
33
+ function verifyUploadToken(token, { secret }) {
34
+ let payload;
35
+ try {
36
+ payload = verifySignedToken(token, {
37
+ purpose: UPLOAD_TOKEN_PURPOSE,
38
+ secret
39
+ });
40
+ } catch (e) {
41
+ if (e instanceof SignedTokenError && e.code === "MISSING_SECRET") {
42
+ throw new UploadError("CONFIGURATION", e.message, e);
43
+ }
44
+ const detail = e instanceof SignedTokenError ? e.code : "INVALID";
45
+ throw new UploadError(
46
+ "INVALID_TOKEN",
47
+ `Invalid upload token (${detail}).`,
48
+ e
49
+ );
50
+ }
51
+ if (!isUploadTokenPayload(payload)) {
52
+ throw new UploadError("INVALID_TOKEN", "Invalid upload token (MALFORMED).");
53
+ }
54
+ return payload;
55
+ }
56
+ export {
57
+ UPLOAD_TOKEN_HEADER,
58
+ UPLOAD_TOKEN_PURPOSE,
59
+ createUploadToken,
60
+ verifyUploadToken
61
+ };