@useupup/server 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/config-3b434689.d.cts +297 -0
- package/dist/config-3b434689.d.ts +297 -0
- package/dist/express.cjs +2360 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +19 -0
- package/dist/express.d.ts +19 -0
- package/dist/express.js +2358 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.cjs +2365 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +32 -0
- package/dist/fastify.d.ts +32 -0
- package/dist/fastify.js +2363 -0
- package/dist/fastify.js.map +1 -0
- package/dist/handler-030ee446.d.cts +6 -0
- package/dist/handler-9f2df17e.d.ts +6 -0
- package/dist/hono.cjs +2314 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +7 -0
- package/dist/hono.d.ts +7 -0
- package/dist/hono.js +2312 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +2349 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +2342 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +2359 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +27 -0
- package/dist/next.d.ts +27 -0
- package/dist/next.js +2355 -0
- package/dist/next.js.map +1 -0
- package/dist/node-http-bridge.cjs +59 -0
- package/dist/node-http-bridge.cjs.map +1 -0
- package/dist/node-http-bridge.d.cts +29 -0
- package/dist/node-http-bridge.d.ts +29 -0
- package/dist/node-http-bridge.js +32 -0
- package/dist/node-http-bridge.js.map +1 -0
- package/package.json +102 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { PresignedUrlResponse, MultipartInitResponse, MultipartSignPartResponse, StorageProvider, UpupCorsConfig } from '@useupup/core';
|
|
2
|
+
|
|
3
|
+
type UpupServerErrorEvent = {
|
|
4
|
+
route: string;
|
|
5
|
+
method: string;
|
|
6
|
+
status: number;
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
requestId?: string | undefined;
|
|
10
|
+
error?: {
|
|
11
|
+
name: string;
|
|
12
|
+
message: string;
|
|
13
|
+
stack?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
type UpupServerLogger = (event: UpupServerErrorEvent) => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Free-form routing hints the CLIENT sends alongside a file's name/type/size,
|
|
20
|
+
* as the `metadata` field of a `/presign`, `/multipart/init`, or drive-transfer
|
|
21
|
+
* body. upup neither interprets nor validates it — it is carried through to
|
|
22
|
+
* `keyStrategy` and the storage resolver verbatim.
|
|
23
|
+
*
|
|
24
|
+
* It is ATTACKER-CONTROLLED. Treat it as you would a query parameter: switch on
|
|
25
|
+
* it against a fixed allow-list, never let it name a bucket, a path prefix, or
|
|
26
|
+
* a credential directly.
|
|
27
|
+
*/
|
|
28
|
+
type UpupClientMetadata = Record<string, unknown>;
|
|
29
|
+
/** Context passed to a custom keyStrategy. */
|
|
30
|
+
interface KeyStrategyContext {
|
|
31
|
+
/** Resolved userId, or null when anonymous. */
|
|
32
|
+
userId: string | null;
|
|
33
|
+
fileName: string;
|
|
34
|
+
contentType: string;
|
|
35
|
+
size: number;
|
|
36
|
+
/** The client's `metadata` field, if it sent one. Untrusted — see
|
|
37
|
+
* {@link UpupClientMetadata}. */
|
|
38
|
+
metadata?: UpupClientMetadata;
|
|
39
|
+
/** The originating request, past every auth and policy check. */
|
|
40
|
+
req: Request;
|
|
41
|
+
}
|
|
42
|
+
/** Which of the three presign-side responses `onPresignResponse` is rewriting. */
|
|
43
|
+
type PresignResponsePhase = 'presign' | 'multipart-init' | 'multipart-sign-part';
|
|
44
|
+
interface PresignResponseContext {
|
|
45
|
+
/** The originating request, already past every auth and policy check. */
|
|
46
|
+
req: Request;
|
|
47
|
+
phase: PresignResponsePhase;
|
|
48
|
+
/** The server-chosen object key. Present on all three phases — on
|
|
49
|
+
* `multipart-sign-part` it comes from the VERIFIED token, not the client. */
|
|
50
|
+
key: string;
|
|
51
|
+
/** The client-declared file (name/type/size). Absent on
|
|
52
|
+
* `multipart-sign-part`, which sees only a token and a part number. */
|
|
53
|
+
file?: FileMetadata;
|
|
54
|
+
/** The client's `metadata` field, if it sent one. Untrusted — see
|
|
55
|
+
* {@link UpupClientMetadata}. Absent on `multipart-sign-part`. */
|
|
56
|
+
metadata?: UpupClientMetadata;
|
|
57
|
+
/** Resolved userId, or null for an anonymous (server-namespaced) upload. */
|
|
58
|
+
userId: string | null;
|
|
59
|
+
}
|
|
60
|
+
/** What the hook receives — narrow it on `ctx.phase`, or with `'uploadUrl' in response`. */
|
|
61
|
+
type PresignResponseBody = PresignedUrlResponse | (MultipartInitResponse & {
|
|
62
|
+
token: string;
|
|
63
|
+
}) | MultipartSignPartResponse;
|
|
64
|
+
/** What the hook may return: the same shapes, plus any extra fields you want
|
|
65
|
+
* to add for your client. */
|
|
66
|
+
type PresignResponseRewrite = PresignResponseBody & Record<string, unknown>;
|
|
67
|
+
type OnPresignResponse = (response: PresignResponseBody, ctx: PresignResponseContext) => PresignResponseRewrite | void | Promise<PresignResponseRewrite | void>;
|
|
68
|
+
/** One bucket's worth of S3 / S3-compatible connection settings. */
|
|
69
|
+
interface UpupStorageConfig {
|
|
70
|
+
/**
|
|
71
|
+
* An S3 / S3-compatible provider label. @useupup/server only speaks the S3
|
|
72
|
+
* API (buildS3ClientConfig always builds an @aws-sdk/client-s3 client) —
|
|
73
|
+
* set `endpoint` for any non-AWS backend (MinIO/R2/DO Spaces/etc). A
|
|
74
|
+
* provider with no S3-compatible surface (currently `StorageProvider.Azure`
|
|
75
|
+
* — see @useupup/core's NON_S3_STORAGE_PROVIDERS) is rejected by
|
|
76
|
+
* createUpupHandler at construct time.
|
|
77
|
+
*/
|
|
78
|
+
type: StorageProvider | string;
|
|
79
|
+
bucket: string;
|
|
80
|
+
region: string;
|
|
81
|
+
accessKeyId?: string;
|
|
82
|
+
secretAccessKey?: string;
|
|
83
|
+
/** S3-compatible endpoint (MinIO / Cloudflare R2 / DO Spaces / on-prem). Omit for AWS S3. */
|
|
84
|
+
endpoint?: string;
|
|
85
|
+
/** Path-style addressing. Defaults to true when `endpoint` is set (required by MinIO).
|
|
86
|
+
* Only applies when `endpoint` is set; ignored for native AWS S3. */
|
|
87
|
+
forcePathStyle?: boolean;
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
}
|
|
90
|
+
/** Which operation is asking for a storage config. */
|
|
91
|
+
type StorageResolverPhase = 'presign' | 'multipart-init' | 'multipart-sign-part' | 'multipart-complete' | 'multipart-abort' | 'multipart-resume' | 'drive-transfer';
|
|
92
|
+
interface StorageResolverContext {
|
|
93
|
+
/** The originating request, past every auth and policy check. */
|
|
94
|
+
req: Request;
|
|
95
|
+
phase: StorageResolverPhase;
|
|
96
|
+
/** Resolved userId, or null for an anonymous (server-namespaced) upload. */
|
|
97
|
+
userId: string | null;
|
|
98
|
+
/** The client's `metadata` field, if it sent one. Untrusted — see
|
|
99
|
+
* {@link UpupClientMetadata}. Absent on the multipart continuation
|
|
100
|
+
* phases, which carry only a token. */
|
|
101
|
+
metadata?: UpupClientMetadata;
|
|
102
|
+
fileName?: string;
|
|
103
|
+
contentType?: string;
|
|
104
|
+
size?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Set on `multipart-sign-part` / `-complete` / `-abort` / `-resume` ONLY:
|
|
107
|
+
* the opaque
|
|
108
|
+
* identity of the storage this upload's `init` resolved, carried inside the
|
|
109
|
+
* HMAC-signed upload token. Return the SAME storage for it — the server
|
|
110
|
+
* re-derives the identity of whatever you return and answers `403
|
|
111
|
+
* AUTH_DENIED` if it does not match, so a continuation can never be
|
|
112
|
+
* steered to a different bucket than the one it started in.
|
|
113
|
+
*/
|
|
114
|
+
storageId?: string;
|
|
115
|
+
}
|
|
116
|
+
type UpupStorageResolver = (ctx: StorageResolverContext) => UpupStorageConfig | Promise<UpupStorageConfig>;
|
|
117
|
+
type UpupServerConfig = {
|
|
118
|
+
/**
|
|
119
|
+
* One static bucket, or a resolver called per request to pick one — three
|
|
120
|
+
* buckets by upload class, a tenant's own bucket, a quarantine bucket for
|
|
121
|
+
* unscanned files. A resolver is validated at RESOLVE time (a bad config
|
|
122
|
+
* fails that request with a 500), not at construct time like the static
|
|
123
|
+
* form.
|
|
124
|
+
*/
|
|
125
|
+
storage: UpupStorageConfig | UpupStorageResolver;
|
|
126
|
+
providers?: {
|
|
127
|
+
googleDrive?: {
|
|
128
|
+
clientId: string;
|
|
129
|
+
clientSecret: string;
|
|
130
|
+
};
|
|
131
|
+
dropbox?: {
|
|
132
|
+
appKey: string;
|
|
133
|
+
appSecret: string;
|
|
134
|
+
};
|
|
135
|
+
oneDrive?: {
|
|
136
|
+
clientId: string;
|
|
137
|
+
clientSecret: string;
|
|
138
|
+
tenantId?: string;
|
|
139
|
+
};
|
|
140
|
+
box?: {
|
|
141
|
+
clientId: string;
|
|
142
|
+
clientSecret: string;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
tokenStore?: TokenStore;
|
|
146
|
+
/**
|
|
147
|
+
* Identify the authenticated user for OAuth + tokenStore scoping.
|
|
148
|
+
* Return null if the request has no authenticated user (OAuth will 401).
|
|
149
|
+
* If omitted, falls back to a singleton 'default' user — fine for demos,
|
|
150
|
+
* unsuitable for multi-tenant production.
|
|
151
|
+
*/
|
|
152
|
+
getUserId?: (req: Request) => Promise<string | null>;
|
|
153
|
+
/**
|
|
154
|
+
* HMAC secret for stateless upload tokens (multipart key/uploadId binding).
|
|
155
|
+
* REQUIRED. Stable, high-entropy, >=16 chars, shared across all instances.
|
|
156
|
+
* `createUpupHandler` throws if missing or too short.
|
|
157
|
+
*/
|
|
158
|
+
uploadTokenSecret?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Override object-key generation. Default namespaces by userId:
|
|
161
|
+
* `<userId|anon>/<uuid>/<sanitized-filename>`. The client never chooses the key.
|
|
162
|
+
*/
|
|
163
|
+
keyStrategy?: (ctx: KeyStrategyContext) => string;
|
|
164
|
+
/**
|
|
165
|
+
* TTL, in SECONDS, for the signed GET download URLs this server hands back
|
|
166
|
+
* (`downloadUrl` on the presign / multipart-complete / drive-transfer
|
|
167
|
+
* responses, and `getDownloadUrl`'s result). Defaults to 3 days. Lower it
|
|
168
|
+
* for gated content — a 15-minute link is `900`. This is the download half
|
|
169
|
+
* only; the upload URL's own 1-hour expiry is unaffected.
|
|
170
|
+
*/
|
|
171
|
+
downloadUrlExpiresIn?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Permit drive providers / tokenStore WITHOUT a getUserId resolver, collapsing
|
|
174
|
+
* every caller into one shared anonymous namespace. Demos only — never in
|
|
175
|
+
* multi-tenant production. Default false -> createUpupHandler throws.
|
|
176
|
+
*/
|
|
177
|
+
allowAnonymous?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Permit `/presign` + `/multipart/init` with no `auth` and no `getUserId`
|
|
180
|
+
* resolver — uploads run under the shared anonymous namespace. Demos /
|
|
181
|
+
* upstream-auth deployments (tus/companion-style, where auth is handled
|
|
182
|
+
* before the request reaches this handler) only. Default false -> those
|
|
183
|
+
* routes return 403 AUTH_REQUIRED.
|
|
184
|
+
*/
|
|
185
|
+
allowAnonymousUploads?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* onFileUploaded/onUploadComplete fire on server-side-completion paths only
|
|
188
|
+
* (multipart-complete, drive transfer) -- direct presigned-PUT uploads never
|
|
189
|
+
* reach the server on completion, so no hook fires for them. See the
|
|
190
|
+
* README's "Lifecycle hooks" section for the full per-path breakdown.
|
|
191
|
+
*/
|
|
192
|
+
hooks?: {
|
|
193
|
+
/**
|
|
194
|
+
* Admission gate. Return `false` to reject with a generic
|
|
195
|
+
* `403 Upload rejected`; THROW an `UpupError` to reject with that
|
|
196
|
+
* error's own message and code in the 403 body (a quota check can say
|
|
197
|
+
* "Storage limit exceeded — upgrade to keep uploading"). Any other
|
|
198
|
+
* throw stays a generic 500 — internal error text never reaches the
|
|
199
|
+
* client.
|
|
200
|
+
*/
|
|
201
|
+
onBeforeUpload?: (file: FileMetadata, req: Request) => Promise<boolean>;
|
|
202
|
+
onFileUploaded?: (file: UploadedFile, req: Request) => Promise<void>;
|
|
203
|
+
onUploadComplete?: (files: UploadedFile[], req: Request) => Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Last look at a presign-side response body before it is sent, for
|
|
206
|
+
* deployments where the storage endpoint is not browser-reachable
|
|
207
|
+
* (a same-origin proxy route, a docker-internal MinIO hostname, a
|
|
208
|
+
* VPC-only endpoint). Return an object to REPLACE the payload; return
|
|
209
|
+
* nothing to leave it as-is.
|
|
210
|
+
*
|
|
211
|
+
* Fires on exactly three responses, identified by `ctx.phase`:
|
|
212
|
+
* `POST /presign` (`presign`), `POST /multipart/init`
|
|
213
|
+
* (`multipart-init`, token already issued), and
|
|
214
|
+
* `POST /multipart/sign-part` (`multipart-sign-part`).
|
|
215
|
+
*
|
|
216
|
+
* It runs AFTER every auth, policy, and token check and cannot bypass
|
|
217
|
+
* any of them — a request that would 401/403 never reaches the hook.
|
|
218
|
+
* Rewriting `uploadUrl` changes where the browser sends bytes, so the
|
|
219
|
+
* URL you substitute must land at the same object.
|
|
220
|
+
*/
|
|
221
|
+
onPresignResponse?: OnPresignResponse;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* How long after its ORIGINAL `/multipart/init` an upload may still be
|
|
225
|
+
* resumed via `POST /multipart/resume`, in seconds. Default 86400 (24h),
|
|
226
|
+
* matching the client's localStorage session TTL. Set `0` to disable the
|
|
227
|
+
* route entirely (it then 404s like any unknown path) — the cost of the
|
|
228
|
+
* route is that a leaked token stays usable for this window, though only to
|
|
229
|
+
* continue the SAME upload, to the SAME key, inside the SAME signed size
|
|
230
|
+
* envelope, and still owner-bound whenever `getUserId` is configured.
|
|
231
|
+
* Resuming re-issues a token with a fresh 1h expiry but carries the original
|
|
232
|
+
* issue time forward, so rolling resumes can never extend this window.
|
|
233
|
+
*/
|
|
234
|
+
multipartResumeWindowSeconds?: number;
|
|
235
|
+
auth?: (req: Request) => Promise<boolean>;
|
|
236
|
+
maxFileSize?: number;
|
|
237
|
+
allowedTypes?: string[];
|
|
238
|
+
cors?: UpupCorsConfig;
|
|
239
|
+
/**
|
|
240
|
+
* Called on every error path (500s, invalid upload tokens, OAuth/token-exchange
|
|
241
|
+
* failures, health-check storage failures). Never receives secrets, tokens,
|
|
242
|
+
* request bodies, or Authorization headers — only a route/method/status/code/
|
|
243
|
+
* message plus the caught error's name/message/stack. Default: logs a
|
|
244
|
+
* structured line via console.error.
|
|
245
|
+
*/
|
|
246
|
+
onError?: UpupServerLogger;
|
|
247
|
+
/** Options for the built-in GET /health route. */
|
|
248
|
+
health?: {
|
|
249
|
+
/**
|
|
250
|
+
* Expose the first 8 hex chars of SHA-256(uploadTokenSecret) on /health so
|
|
251
|
+
* operators can spot cross-instance secret drift without revealing the
|
|
252
|
+
* secret itself. Default: false.
|
|
253
|
+
*/
|
|
254
|
+
exposeSecretFingerprint?: boolean;
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Key-value store the server uses for OAuth state + drive access tokens.
|
|
259
|
+
* Interface matches Redis / Cloudflare KV / any string-keyed KV.
|
|
260
|
+
* Consumers implement this against their own persistence layer.
|
|
261
|
+
*/
|
|
262
|
+
interface TokenStore {
|
|
263
|
+
get(key: string): Promise<string | null>;
|
|
264
|
+
set(key: string, value: string, ttlSeconds?: number): Promise<void>;
|
|
265
|
+
delete(key: string): Promise<void>;
|
|
266
|
+
}
|
|
267
|
+
/** Drive OAuth tokens we persist after a successful /auth/:provider/cb. */
|
|
268
|
+
interface DriveTokens {
|
|
269
|
+
accessToken: string;
|
|
270
|
+
expiresAt?: number | undefined;
|
|
271
|
+
scope?: string | undefined;
|
|
272
|
+
tokenType?: string | undefined;
|
|
273
|
+
refreshToken?: string | undefined;
|
|
274
|
+
}
|
|
275
|
+
/** Short-lived OAuth state map, keyed by the random state param. */
|
|
276
|
+
interface OAuthState {
|
|
277
|
+
userId: string;
|
|
278
|
+
provider: string;
|
|
279
|
+
returnTo?: string | undefined;
|
|
280
|
+
}
|
|
281
|
+
interface FileMetadata {
|
|
282
|
+
name: string;
|
|
283
|
+
size: number;
|
|
284
|
+
type: string;
|
|
285
|
+
/** Free-form routing hints from the client. Untrusted — see
|
|
286
|
+
* {@link UpupClientMetadata}. */
|
|
287
|
+
metadata?: UpupClientMetadata;
|
|
288
|
+
}
|
|
289
|
+
interface UploadedFile {
|
|
290
|
+
key: string;
|
|
291
|
+
name: string;
|
|
292
|
+
size: number;
|
|
293
|
+
type: string;
|
|
294
|
+
url: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export type { DriveTokens as D, FileMetadata as F, KeyStrategyContext as K, OAuthState as O, PresignResponsePhase as P, StorageResolverPhase as S, TokenStore as T, UpupServerConfig as U, UpupStorageConfig as a, UploadedFile as b, PresignResponseContext as c, PresignResponseBody as d, PresignResponseRewrite as e, OnPresignResponse as f, UpupClientMetadata as g, UpupStorageResolver as h, StorageResolverContext as i };
|