@nextlyhq/storage-uploadthing 0.0.2-alpha.60 → 0.0.2-alpha.64

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/dist/index.cjs CHANGED
@@ -1,16 +1,1013 @@
1
1
  'use strict';
2
2
 
3
+ var module$1 = require('module');
3
4
  var server = require('uploadthing/server');
4
5
 
6
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
5
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
8
  var __esm = (fn, res) => function __init() {
7
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
10
  };
9
11
 
10
- // ../nextly/dist/chunk-2ROPMX7N.mjs
12
+ // ../nextly/dist/chunk-HJ5IQPOY.mjs
13
+ function isDbError(err) {
14
+ if (!err || typeof err !== "object") return false;
15
+ const obj = err;
16
+ return obj.name === "DbError" && typeof obj.kind === "string";
17
+ }
18
+ function hasBrand(value) {
19
+ if (value === null || typeof value !== "object" && typeof value !== "function") {
20
+ return false;
21
+ }
22
+ return value[NEXTLY_ERROR_BRAND] === true;
23
+ }
24
+ var NEXTLY_ERROR_STATUS, CANONICAL_CODE_FOR_STATUS, LEGACY_UNPROCESSABLE_STATUS, DB_ERROR_MAPPING, NEXTLY_ERROR_BRAND, NextlyError;
25
+ var init_chunk_HJ5IQPOY = __esm({
26
+ "../nextly/dist/chunk-HJ5IQPOY.mjs"() {
27
+ NEXTLY_ERROR_STATUS = {
28
+ VALIDATION_ERROR: 400,
29
+ INVALID_INPUT: 400,
30
+ AUTH_REQUIRED: 401,
31
+ AUTH_INVALID_CREDENTIALS: 401,
32
+ TOKEN_EXPIRED: 401,
33
+ FORBIDDEN: 403,
34
+ // The schema builder is off in this environment (production by default).
35
+ // Separate from FORBIDDEN: the caller's permissions are not the problem.
36
+ BUILDER_DISABLED: 403,
37
+ NOT_FOUND: 404,
38
+ CONFLICT: 409,
39
+ DUPLICATE: 409,
40
+ RATE_LIMITED: 429,
41
+ PAYLOAD_TOO_LARGE: 413,
42
+ UNSUPPORTED_MEDIA_TYPE: 415,
43
+ // 422: understood, well-formed, and refused on a rule the caller can act on.
44
+ // Deliberately NOT added to CANONICAL_CODE_FOR_STATUS -- that list drives the
45
+ // status -> code direction, where 422 stays mapped to INVALID_INPUT.
46
+ BUSINESS_RULE_VIOLATION: 422,
47
+ INTERNAL_ERROR: 500,
48
+ DATABASE_ERROR: 500,
49
+ EXTERNAL_SERVICE_ERROR: 502,
50
+ SERVICE_UNAVAILABLE: 503,
51
+ // Outbound-fetch safety (utils/validate-external-url): a URL refused for SSRF
52
+ // safety, and a fetch that timed out / exceeded the size cap / failed to decode.
53
+ EXTERNAL_URL_BLOCKED: 400,
54
+ EXTERNAL_REQUEST_FAILED: 502,
55
+ FILENAME_INVALID: 400,
56
+ EXTENSION_BLOCKED: 400,
57
+ MIME_BLOCKED: 415,
58
+ MIME_NOT_ALLOWED: 415,
59
+ SIZE_EXCEEDED: 413,
60
+ // A stored object exceeded the cap a READ was given, which is a different
61
+ // question from SIZE_EXCEEDED above: that one refuses an upload the caller
62
+ // is sending, this one refuses to buffer an object already stored. Kept
63
+ // apart so a caller discriminating on the code cannot match both.
64
+ STORAGE_READ_TOO_LARGE: 413,
65
+ // A stored object did not answer within the deadline the read was given. 504
66
+ // rather than 500 because the failure is the BACKEND not answering, and a
67
+ // caller can act on that difference: a gateway timeout is worth retrying, an
68
+ // internal error is not.
69
+ STORAGE_READ_TIMEOUT: 504,
70
+ // The store answered, and answered badly. 502 rather than 500 because the
71
+ // fault is UPSTREAM of this process: a caller can retry it, and an operator
72
+ // reading the log needs to look at the bucket rather than at this service.
73
+ // Distinct from the timeout above, which never got an answer at all.
74
+ STORAGE_READ_UNREACHABLE: 502,
75
+ MAGIC_BYTE_MISMATCH: 400,
76
+ SVG_SANITIZATION_FAILED: 400,
77
+ UNSUPPORTED_FOR_BACKEND: 415,
78
+ // Plan B — schema bookkeeping consolidation.
79
+ NEXTLY_LEGACY_BOOKKEEPING_DETECTED: 409,
80
+ NEXTLY_UPGRADE_TABLE_NAME_COLLISION: 409,
81
+ NEXTLY_UPGRADE_IN_PROGRESS: 409,
82
+ // Plan C2 — nextly migrate phases.
83
+ NEXTLY_MIGRATE_LOCK_BUSY: 409,
84
+ NEXTLY_BASELINE_LOCK_NOT_HELD: 409,
85
+ NEXTLY_RESOLVE_LOCK_NOT_HELD: 409,
86
+ // Boot refused to serve: the migrate lock stayed held past the wait deadline,
87
+ // so this process never established whether the schema matches the code. 503
88
+ // rather than 409 — a load balancer should take the instance out of rotation
89
+ // and retry it, which is exactly the recovery this refusal wants.
90
+ NEXTLY_BOOT_MIGRATIONS_NOT_RUN: 503,
91
+ // Still running rather than refused. 503 for the same reason: retry shortly.
92
+ NEXTLY_BOOT_MIGRATIONS_PENDING: 503,
93
+ NEXTLY_CORE_DESTRUCTIVE_REFUSED: 409,
94
+ NEXTLY_MIGRATION_DRIFT: 409,
95
+ NEXTLY_MIGRATION_APPLY_FAILED: 500,
96
+ // Plan C3 — migrate:resolve recovery command.
97
+ NEXTLY_MIGRATION_FILE_MISSING: 404,
98
+ NEXTLY_MIGRATION_SNAPSHOT_MISSING: 404,
99
+ NEXTLY_MIGRATION_RESOLVE_DRIFT: 409,
100
+ NEXTLY_MIGRATION_RESOLVE_PRECONDITION: 409,
101
+ // Plan D — UI schema support.
102
+ NEXTLY_UI_SCHEMA_INVALID: 400,
103
+ NEXTLY_SCHEMA_SLUG_COLLISION: 409,
104
+ NEXTLY_SCHEMA_RELATION_TARGET_MISSING: 400,
105
+ // Plugin platform (P2b) — schema extend (contributes.extend) + relations (D15).
106
+ NEXTLY_SCHEMA_EXTEND_TARGET_UNKNOWN: 400,
107
+ NEXTLY_SCHEMA_EXTEND_FIELD_DUPLICATE: 409,
108
+ NEXTLY_SCHEMA_CROSS_PLUGIN_RELATION: 409,
109
+ // Plugin platform (P2c) — framework remap (.rename()).
110
+ NEXTLY_SCHEMA_RENAME_UNKNOWN_TARGET: 400,
111
+ // Plugin platform — a declared admin.clientConfig that cannot be delivered
112
+ // to the browser, refused at boot rather than serialized mangled.
113
+ NEXTLY_PLUGIN_CLIENT_CONFIG_INVALID: 500,
114
+ // Plugin platform — a contributed admin widget that cannot be delivered to
115
+ // the browser. Refused at boot because it is serialized into the ONE
116
+ // `/api/admin-meta/workspace` payload: a value `JSON.stringify` throws on
117
+ // fails that request for every admin, not just the widget's own card.
118
+ NEXTLY_PLUGIN_ADMIN_WIDGET_INVALID: 500,
119
+ // Plugin platform (P0) — boot-time plugin dependency/version resolution.
120
+ PLUGIN_RESOLUTION_ERROR: 500,
121
+ // Plugin platform (P4) — contributes.routes collection (D25).
122
+ NEXTLY_ROUTE_COLLISION: 409,
123
+ NEXTLY_ROUTE_INVALID_PATH: 400,
124
+ // An email transport whose library is an optional peer dependency the host
125
+ // has not installed. 503 rather than 500: the request is not malformed and
126
+ // nothing is broken, the install simply cannot carry it out yet, and the
127
+ // remedy is one command on the server rather than a change by the caller.
128
+ NEXTLY_EMAIL_TRANSPORT_UNAVAILABLE: 503,
129
+ // The tooling that compiles `nextly.config.ts` is an optional peer the host
130
+ // has not installed. 503 rather than 500 for the same reason as the mail
131
+ // transport above: nothing is broken and the request is not malformed, the
132
+ // install simply cannot carry it out until one command is run.
133
+ NEXTLY_CONFIG_TOOLING_UNAVAILABLE: 503
134
+ };
135
+ CANONICAL_CODE_FOR_STATUS = [
136
+ "VALIDATION_ERROR",
137
+ "AUTH_REQUIRED",
138
+ "FORBIDDEN",
139
+ "NOT_FOUND",
140
+ "CONFLICT",
141
+ "PAYLOAD_TOO_LARGE",
142
+ "UNSUPPORTED_MEDIA_TYPE",
143
+ "RATE_LIMITED",
144
+ "EXTERNAL_SERVICE_ERROR",
145
+ "SERVICE_UNAVAILABLE"
146
+ ];
147
+ LEGACY_UNPROCESSABLE_STATUS = 422;
148
+ ({
149
+ ...Object.fromEntries(
150
+ CANONICAL_CODE_FOR_STATUS.map((code) => [NEXTLY_ERROR_STATUS[code], code])
151
+ ),
152
+ [LEGACY_UNPROCESSABLE_STATUS]: "INVALID_INPUT"
153
+ });
154
+ DB_ERROR_MAPPING = {
155
+ "unique-violation": {
156
+ code: "DUPLICATE",
157
+ statusCode: 409,
158
+ publicMessage: "Resource already exists."
159
+ },
160
+ "fk-violation": {
161
+ code: "VALIDATION_ERROR",
162
+ statusCode: 400,
163
+ publicMessage: "Referenced record does not exist."
164
+ },
165
+ "not-null-violation": {
166
+ code: "VALIDATION_ERROR",
167
+ statusCode: 400,
168
+ publicMessage: "A required field is missing."
169
+ },
170
+ constraint: {
171
+ code: "VALIDATION_ERROR",
172
+ statusCode: 400,
173
+ publicMessage: "The provided data violates a constraint."
174
+ },
175
+ deadlock: {
176
+ code: "CONFLICT",
177
+ statusCode: 409,
178
+ publicMessage: "The operation could not be completed. Please retry."
179
+ },
180
+ "serialization-failure": {
181
+ code: "CONFLICT",
182
+ statusCode: 409,
183
+ publicMessage: "The operation could not be completed. Please retry."
184
+ },
185
+ timeout: {
186
+ code: "DATABASE_ERROR",
187
+ statusCode: 500,
188
+ publicMessage: "The operation timed out. Please try again."
189
+ },
190
+ "connection-lost": {
191
+ code: "DATABASE_ERROR",
192
+ statusCode: 500,
193
+ publicMessage: "A temporary database error occurred. Please try again."
194
+ },
195
+ syntax: {
196
+ code: "INTERNAL_ERROR",
197
+ statusCode: 500,
198
+ publicMessage: "An unexpected error occurred."
199
+ },
200
+ internal: {
201
+ code: "INTERNAL_ERROR",
202
+ statusCode: 500,
203
+ publicMessage: "An unexpected error occurred."
204
+ }
205
+ };
206
+ NEXTLY_ERROR_BRAND = Symbol.for("nextly/NextlyError");
207
+ NextlyError = class _NextlyError extends Error {
208
+ code;
209
+ statusCode;
210
+ publicMessage;
211
+ publicData;
212
+ messageKey;
213
+ logMessage;
214
+ logContext;
215
+ cause;
216
+ timestamp;
217
+ constructor(opts) {
218
+ super(opts.publicMessage);
219
+ this.name = "NextlyError";
220
+ this.code = opts.code;
221
+ this.publicMessage = opts.publicMessage;
222
+ this.publicData = opts.publicData;
223
+ this.messageKey = opts.messageKey;
224
+ this.logMessage = opts.logMessage;
225
+ this.logContext = opts.logContext;
226
+ this.cause = opts.cause;
227
+ this.timestamp = /* @__PURE__ */ new Date();
228
+ this.statusCode = _NextlyError.resolveStatusCode(opts);
229
+ Error.captureStackTrace?.(this, _NextlyError);
230
+ }
231
+ static {
232
+ _NextlyError.prototype[NEXTLY_ERROR_BRAND] = true;
233
+ }
234
+ static resolveStatusCode(opts) {
235
+ if (typeof opts.statusCode === "number") return opts.statusCode;
236
+ if (opts.code in NEXTLY_ERROR_STATUS) {
237
+ return NEXTLY_ERROR_STATUS[opts.code];
238
+ }
239
+ return 500;
240
+ }
241
+ /** HTTP-safe JSON. Strips logMessage / logContext / cause / stack. */
242
+ toResponseJSON(requestId) {
243
+ const json = {
244
+ code: String(this.code),
245
+ message: this.publicMessage,
246
+ requestId
247
+ };
248
+ if (this.messageKey) json.messageKey = this.messageKey;
249
+ if (this.publicData !== void 0) json.data = this.publicData;
250
+ return json;
251
+ }
252
+ /** Operator-facing JSON for log lines. Includes everything. */
253
+ toLogJSON(requestId) {
254
+ return {
255
+ code: this.code,
256
+ statusCode: this.statusCode,
257
+ publicMessage: this.publicMessage,
258
+ messageKey: this.messageKey,
259
+ logMessage: this.logMessage,
260
+ logContext: this.logContext,
261
+ cause: this.cause ? {
262
+ name: this.cause.name,
263
+ message: this.cause.message,
264
+ stack: this.cause.stack
265
+ } : void 0,
266
+ timestamp: this.timestamp.toISOString(),
267
+ requestId
268
+ };
269
+ }
270
+ // ────────────────────────────────────────────────────────────────────
271
+ // Static factories — the recommended throw site for every common case.
272
+ // Public messages here follow the §13.8 rubric: complete sentence,
273
+ // generic, no identifiers, no value echoing, no policy hints.
274
+ // ────────────────────────────────────────────────────────────────────
275
+ static invalidCredentials(opts) {
276
+ return new _NextlyError({
277
+ code: "AUTH_INVALID_CREDENTIALS",
278
+ publicMessage: "Invalid email or password.",
279
+ logMessage: "Login failed",
280
+ logContext: opts?.logContext
281
+ });
282
+ }
283
+ static authRequired(opts) {
284
+ return new _NextlyError({
285
+ code: "AUTH_REQUIRED",
286
+ publicMessage: "Authentication required.",
287
+ logContext: opts?.logContext
288
+ });
289
+ }
290
+ /**
291
+ * Distinct from `authRequired`: the caller was authenticated but the
292
+ * session token expired. Clients key on the TOKEN_EXPIRED *code* to
293
+ * silently refresh and retry rather than redirecting to login; the public
294
+ * message stays the generic spec §13.6 string ("Authentication required.")
295
+ * so the wire never reveals the session state — same as `authRequired`.
296
+ */
297
+ static tokenExpired(opts) {
298
+ return new _NextlyError({
299
+ code: "TOKEN_EXPIRED",
300
+ publicMessage: "Authentication required.",
301
+ logContext: opts?.logContext
302
+ });
303
+ }
304
+ static notFound(opts) {
305
+ return new _NextlyError({
306
+ code: "NOT_FOUND",
307
+ publicMessage: opts?.message ?? "Not found.",
308
+ cause: opts?.cause,
309
+ logContext: opts?.logContext
310
+ });
311
+ }
312
+ static forbidden(opts) {
313
+ return new _NextlyError({
314
+ code: "FORBIDDEN",
315
+ publicMessage: "You don't have permission to perform this action.",
316
+ cause: opts?.cause,
317
+ logContext: opts?.logContext
318
+ });
319
+ }
320
+ /**
321
+ * A call the caller got wrong, where naming the mistake IS the value of the
322
+ * error.
323
+ *
324
+ * The only factory that takes its public message from the caller. The
325
+ * generic messages elsewhere exist so an HTTP response cannot leak internal
326
+ * detail; this one is for arguments and configuration a developer controls
327
+ * and must be told about — a missing option, an unusable combination — where
328
+ * `internal()` would reduce the one useful sentence to "An unexpected error
329
+ * occurred." Do not pass user-supplied data through it.
330
+ */
331
+ static invalidInput(opts) {
332
+ return new _NextlyError({
333
+ code: "INVALID_INPUT",
334
+ publicMessage: opts.message,
335
+ logContext: opts.logContext
336
+ });
337
+ }
338
+ static validation(opts) {
339
+ return new _NextlyError({
340
+ code: "VALIDATION_ERROR",
341
+ publicMessage: "Validation failed.",
342
+ publicData: { errors: opts.errors },
343
+ cause: opts.cause,
344
+ logContext: opts.logContext
345
+ });
346
+ }
347
+ static conflict(opts) {
348
+ return new _NextlyError({
349
+ code: "CONFLICT",
350
+ publicMessage: opts?.message ?? "The resource has changed since you last loaded it. Please refresh and try again.",
351
+ cause: opts?.cause,
352
+ logContext: { reason: opts?.reason, ...opts?.logContext }
353
+ });
354
+ }
355
+ static duplicate(opts) {
356
+ return new _NextlyError({
357
+ code: "DUPLICATE",
358
+ publicMessage: "Resource already exists.",
359
+ logContext: opts?.logContext
360
+ });
361
+ }
362
+ static rateLimited(opts) {
363
+ return new _NextlyError({
364
+ code: "RATE_LIMITED",
365
+ publicMessage: "Too many requests. Please try again later.",
366
+ publicData: opts?.retryAfterSeconds !== void 0 ? { retryAfterSeconds: opts.retryAfterSeconds } : void 0,
367
+ logContext: opts?.logContext
368
+ });
369
+ }
370
+ static internal(opts) {
371
+ return new _NextlyError({
372
+ code: "INTERNAL_ERROR",
373
+ publicMessage: "An unexpected error occurred.",
374
+ cause: opts?.cause,
375
+ logContext: opts?.logContext
376
+ });
377
+ }
378
+ // Accepts an optional `logMessage` override so callers (e.g. the health
379
+ // route) can record a specific operator narrative ("Health check failed")
380
+ // while the public message stays canonical per spec §13.8.5.
381
+ static serviceUnavailable(opts) {
382
+ return new _NextlyError({
383
+ code: "SERVICE_UNAVAILABLE",
384
+ publicMessage: opts?.publicMessage ?? "Service unavailable. Please try again later.",
385
+ logMessage: opts?.logMessage,
386
+ cause: opts?.cause,
387
+ logContext: opts?.logContext
388
+ });
389
+ }
390
+ /**
391
+ * Convert a DbError (or arbitrary unknown thrown by the DB layer) to a
392
+ * NextlyError with a generic public message and rich logContext. Used by
393
+ * `withDbErrors` for auto-conversion (Pattern A) and by services that
394
+ * catch DB errors at boundaries (Pattern B). Spec §8.2 mapping table.
395
+ *
396
+ * Never leaks DB driver text, constraint names, or table names into
397
+ * `publicMessage`. All DB context goes into `logContext`. The original
398
+ * DbError is preserved as `cause`.
399
+ */
400
+ static fromDatabaseError(error) {
401
+ if (isDbError(error)) {
402
+ const mapping = DB_ERROR_MAPPING[error.kind];
403
+ const logContext = {
404
+ dbKind: error.kind,
405
+ dialect: error.dialect
406
+ };
407
+ if (error.code !== void 0) logContext.dbCode = error.code;
408
+ if (error.meta !== void 0) logContext.meta = error.meta;
409
+ return new _NextlyError({
410
+ code: mapping.code,
411
+ statusCode: mapping.statusCode,
412
+ publicMessage: mapping.publicMessage,
413
+ logMessage: "Database error",
414
+ logContext,
415
+ cause: error
416
+ });
417
+ }
418
+ return new _NextlyError({
419
+ code: "INTERNAL_ERROR",
420
+ statusCode: 500,
421
+ publicMessage: "An unexpected error occurred.",
422
+ logMessage: "Non-DbError passed to fromDatabaseError",
423
+ cause: error instanceof Error ? error : void 0,
424
+ logContext: error instanceof Error ? void 0 : { value: String(error) }
425
+ });
426
+ }
427
+ // ────────────────────────────────────────────────────────────────────
428
+ // Type guards. Structural rather than `instanceof` so they survive
429
+ // package-boundary mismatches (one consumer's NextlyError is a
430
+ // different module instance from another's).
431
+ // ────────────────────────────────────────────────────────────────────
432
+ static is(err) {
433
+ return hasBrand(err);
434
+ }
435
+ static isCode(err, code) {
436
+ return hasBrand(err) && err.code === code;
437
+ }
438
+ static isNotFound(err) {
439
+ return _NextlyError.isCode(err, "NOT_FOUND");
440
+ }
441
+ static isValidation(err) {
442
+ return _NextlyError.isCode(err, "VALIDATION_ERROR");
443
+ }
444
+ static isAuthRequired(err) {
445
+ return _NextlyError.isCode(err, "AUTH_REQUIRED");
446
+ }
447
+ static isForbidden(err) {
448
+ return _NextlyError.isCode(err, "FORBIDDEN");
449
+ }
450
+ static isConflict(err) {
451
+ return _NextlyError.isCode(err, "CONFLICT");
452
+ }
453
+ static isRateLimited(err) {
454
+ return _NextlyError.isCode(err, "RATE_LIMITED");
455
+ }
456
+ };
457
+ }
458
+ });
459
+
460
+ // ../nextly/dist/chunk-W77AQUUQ.mjs
461
+ var StorageReadTooLargeError;
462
+ var init_chunk_W77AQUUQ = __esm({
463
+ "../nextly/dist/chunk-W77AQUUQ.mjs"() {
464
+ init_chunk_HJ5IQPOY();
465
+ StorageReadTooLargeError = class extends NextlyError {
466
+ constructor(path, maxBytes, size) {
467
+ super({
468
+ code: "STORAGE_READ_TOO_LARGE",
469
+ publicMessage: "The stored file is larger than the limit for this read.",
470
+ logContext: {
471
+ path,
472
+ maxBytes,
473
+ ...size === void 0 ? {} : { size }
474
+ }
475
+ });
476
+ this.path = path;
477
+ this.maxBytes = maxBytes;
478
+ this.size = size;
479
+ }
480
+ };
481
+ }
482
+ });
483
+
484
+ // ../nextly/dist/chunk-C2VHLQKK.mjs
485
+ async function validateExternalUrl(rawUrl, options = {}) {
486
+ let parsed;
487
+ try {
488
+ parsed = new URL(rawUrl);
489
+ } catch {
490
+ throw new ExternalUrlError("Invalid URL", rawUrl);
491
+ }
492
+ const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
493
+ const isLocalhostHostname = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
494
+ const baseProtocols = options.allowedProtocols ?? ["https:"];
495
+ const protocols = options.allowLocalhost && isLocalhostHostname ? [...baseProtocols, "http:"] : baseProtocols;
496
+ if (!protocols.includes(parsed.protocol)) {
497
+ throw new ExternalUrlError(
498
+ `Protocol ${parsed.protocol} not allowed (allowed: ${protocols.join(", ")})`,
499
+ rawUrl
500
+ );
501
+ }
502
+ if (CLOUD_METADATA_HOSTS.has(hostname)) {
503
+ throw new ExternalUrlError(
504
+ `Cloud-metadata hostname rejected: ${parsed.hostname}`,
505
+ rawUrl
506
+ );
507
+ }
508
+ if (IPV4_RE.test(hostname)) {
509
+ if (!isPublicIpv4(
510
+ hostname,
511
+ options.allowLocalhost === true && hostname === "127.0.0.1"
512
+ )) {
513
+ throw new ExternalUrlError(
514
+ `Resolved to non-public IP: ${hostname}`,
515
+ rawUrl
516
+ );
517
+ }
518
+ return { url: parsed, pinnedIp: hostname, family: 4 };
519
+ }
520
+ if (hostname.includes(":")) {
521
+ if (!isPublicIpv6(
522
+ hostname,
523
+ options.allowLocalhost === true && hostname === "::1"
524
+ )) {
525
+ throw new ExternalUrlError(
526
+ `Resolved to non-public IP: ${hostname}`,
527
+ rawUrl
528
+ );
529
+ }
530
+ return { url: parsed, pinnedIp: hostname, family: 6 };
531
+ }
532
+ const { lookup } = await import('dns/promises');
533
+ let addresses;
534
+ try {
535
+ addresses = await lookup(hostname, { all: true });
536
+ } catch (err) {
537
+ throw new ExternalUrlError(
538
+ `DNS lookup failed: ${err.message}`,
539
+ rawUrl
540
+ );
541
+ }
542
+ if (addresses.length === 0) {
543
+ throw new ExternalUrlError("No IPs returned for host", rawUrl);
544
+ }
545
+ for (const { address, family } of addresses) {
546
+ const allow = options.allowLocalhost === true && isLocalhostHostname && family === 4;
547
+ const ok = family === 4 ? isPublicIpv4(address, allow) : isPublicIpv6(address, allow);
548
+ if (!ok) {
549
+ throw new ExternalUrlError(
550
+ `Resolved to non-public IP: ${address}`,
551
+ rawUrl
552
+ );
553
+ }
554
+ }
555
+ const first = addresses[0];
556
+ return {
557
+ url: parsed,
558
+ pinnedIp: first.address,
559
+ family: first.family === 6 ? 6 : 4
560
+ };
561
+ }
562
+ function createPinnedLookup(ip, family) {
563
+ return (_hostname, options, callback) => {
564
+ if (options && options.all) {
565
+ callback(null, [{ address: ip, family }]);
566
+ } else {
567
+ callback(null, ip, family);
568
+ }
569
+ };
570
+ }
571
+ async function safeFetch(rawUrl, options = {}) {
572
+ const {
573
+ allowLocalhost,
574
+ allowedProtocols,
575
+ method,
576
+ headers,
577
+ body,
578
+ signal,
579
+ maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,
580
+ timeoutMs = DEFAULT_TIMEOUT_MS
581
+ } = options;
582
+ const controller = new AbortController();
583
+ const forwardAbort = () => controller.abort(signal?.reason);
584
+ if (signal) {
585
+ if (signal.aborted) controller.abort(signal.reason);
586
+ else signal.addEventListener("abort", forwardAbort, { once: true });
587
+ }
588
+ const timer = setTimeout(
589
+ () => controller.abort(
590
+ new SafeFetchError(`Request exceeded ${timeoutMs}ms`, rawUrl, "timeout")
591
+ ),
592
+ timeoutMs
593
+ );
594
+ try {
595
+ const validated = await abortable(
596
+ validateExternalUrl(rawUrl, { allowLocalhost, allowedProtocols }),
597
+ controller.signal
598
+ );
599
+ return await pinnedFetch(validated, {
600
+ method,
601
+ headers,
602
+ body,
603
+ signal: controller.signal,
604
+ maxResponseBytes
605
+ });
606
+ } finally {
607
+ clearTimeout(timer);
608
+ if (signal) signal.removeEventListener("abort", forwardAbort);
609
+ }
610
+ }
611
+ function abortError(signal) {
612
+ const reason = signal.reason;
613
+ if (reason instanceof Error) return reason;
614
+ return new DOMException("The operation was aborted", "AbortError");
615
+ }
616
+ function abortable(promise, signal) {
617
+ if (signal.aborted) return Promise.reject(abortError(signal));
618
+ let onAbort;
619
+ const aborted = new Promise((_resolve, reject) => {
620
+ onAbort = () => reject(abortError(signal));
621
+ signal.addEventListener("abort", onAbort, { once: true });
622
+ });
623
+ return Promise.race([promise, aborted]).finally(
624
+ () => signal.removeEventListener("abort", onAbort)
625
+ );
626
+ }
627
+ async function pinnedFetch(validated, init) {
628
+ const { url, pinnedIp, family } = validated;
629
+ const httpMod = url.protocol === "https:" ? await import('https') : await import('http');
630
+ const zlib = await import('zlib');
631
+ const lookup = createPinnedLookup(pinnedIp, family);
632
+ const outHeaders = toOutgoingHeaders(init.headers);
633
+ if (init.body != null) {
634
+ delete outHeaders["transfer-encoding"];
635
+ outHeaders["content-length"] = String(
636
+ typeof init.body === "string" ? Buffer.byteLength(init.body) : init.body.byteLength
637
+ );
638
+ }
639
+ return new Promise((resolve, reject) => {
640
+ let settled = false;
641
+ const settle = (fn) => {
642
+ if (settled) return;
643
+ settled = true;
644
+ fn();
645
+ };
646
+ const failure = (err) => init.signal.aborted ? abortError(init.signal) : err;
647
+ const req = httpMod.request(
648
+ url,
649
+ {
650
+ method: init.method ?? "GET",
651
+ headers: outHeaders,
652
+ lookup,
653
+ // Fresh socket per request so a pooled connection can't reuse a
654
+ // differently-resolved address, and so the pinned lookup always runs.
655
+ agent: false,
656
+ // The combined deadline/caller signal aborts the request in flight.
657
+ signal: init.signal
658
+ },
659
+ (res) => {
660
+ const chunks = [];
661
+ let received = 0;
662
+ res.on("data", (chunk) => {
663
+ received += chunk.length;
664
+ if (received > init.maxResponseBytes) {
665
+ res.destroy();
666
+ req.destroy();
667
+ settle(
668
+ () => reject(
669
+ new SafeFetchError(
670
+ `Response body exceeded ${init.maxResponseBytes} bytes`,
671
+ url.href,
672
+ "response-too-large",
673
+ res.statusCode
674
+ )
675
+ )
676
+ );
677
+ return;
678
+ }
679
+ chunks.push(chunk);
680
+ });
681
+ res.on("end", () => {
682
+ const decoded = decodeBody(
683
+ Buffer.concat(chunks),
684
+ res.headers["content-encoding"],
685
+ zlib,
686
+ init.maxResponseBytes,
687
+ url.href,
688
+ res.statusCode
689
+ );
690
+ if (decoded instanceof SafeFetchError) {
691
+ settle(() => reject(decoded));
692
+ return;
693
+ }
694
+ settle(
695
+ () => resolve(toWhatwgResponse(res, decoded.body, decoded.decoded))
696
+ );
697
+ });
698
+ res.on("error", (err) => settle(() => reject(failure(err))));
699
+ }
700
+ );
701
+ req.on("error", (err) => settle(() => reject(failure(err))));
702
+ if (!req.destroyed) {
703
+ if (init.body != null) req.write(init.body);
704
+ req.end();
705
+ }
706
+ });
707
+ }
708
+ function toOutgoingHeaders(headers) {
709
+ const out = {};
710
+ if (!headers) return out;
711
+ const add = (rawKey, value) => {
712
+ const key = rawKey.toLowerCase();
713
+ if (key === "host") return;
714
+ const existing = out[key];
715
+ out[key] = existing === void 0 ? value : [].concat(existing, value);
716
+ };
717
+ if (headers instanceof Headers) {
718
+ headers.forEach((value, key) => add(key, value));
719
+ } else if (Array.isArray(headers)) {
720
+ for (const [key, value] of headers) add(key, value);
721
+ } else {
722
+ for (const [key, value] of Object.entries(headers)) add(key, value);
723
+ }
724
+ return out;
725
+ }
726
+ function toWhatwgResponse(res, body, stripContentHeaders) {
727
+ const headers = new Headers();
728
+ for (const [key, value] of Object.entries(res.headers)) {
729
+ if (value == null) continue;
730
+ const lower = key.toLowerCase();
731
+ if (stripContentHeaders && (lower === "content-encoding" || lower === "content-length")) {
732
+ continue;
733
+ }
734
+ const values = Array.isArray(value) ? value : [value];
735
+ for (const v of values) {
736
+ try {
737
+ headers.append(key, v);
738
+ } catch {
739
+ }
740
+ }
741
+ }
742
+ const raw = res.statusCode ?? 502;
743
+ const status = raw >= 200 && raw <= 599 ? raw : 502;
744
+ const nullBody = status === 204 || status === 205 || status === 304;
745
+ const bytes = new Uint8Array(body.byteLength);
746
+ bytes.set(body);
747
+ return new Response(nullBody ? null : bytes, {
748
+ status,
749
+ statusText: res.statusMessage ?? "",
750
+ headers
751
+ });
752
+ }
753
+ function decodeBody(buf, encoding, zlib, cap, url, status) {
754
+ if (!encoding) return { body: buf, decoded: false };
755
+ if (buf.length === 0) return { body: buf, decoded: true };
756
+ const layers = encoding.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean);
757
+ const opts = { maxOutputLength: cap };
758
+ let current = buf;
759
+ let decoded = false;
760
+ try {
761
+ for (let i = layers.length - 1; i >= 0; i--) {
762
+ const enc = layers[i];
763
+ if (enc === "identity") continue;
764
+ if (enc === "gzip" || enc === "x-gzip") {
765
+ current = zlib.gunzipSync(current, opts);
766
+ } else if (enc === "br") {
767
+ current = zlib.brotliDecompressSync(current, opts);
768
+ } else if (enc === "deflate") {
769
+ try {
770
+ current = zlib.inflateSync(current, opts);
771
+ } catch (inflateErr) {
772
+ if (errorCode(inflateErr) === "ERR_BUFFER_TOO_LARGE")
773
+ throw inflateErr;
774
+ current = zlib.inflateRawSync(current, opts);
775
+ }
776
+ } else {
777
+ if (decoded) {
778
+ return new SafeFetchError(
779
+ `Cannot fully decode content-encoding "${encoding}"`,
780
+ url,
781
+ "decode-failed",
782
+ status
783
+ );
784
+ }
785
+ return { body: buf, decoded: false };
786
+ }
787
+ decoded = true;
788
+ }
789
+ return { body: current, decoded };
790
+ } catch (err) {
791
+ const tooLarge = errorCode(err) === "ERR_BUFFER_TOO_LARGE";
792
+ return new SafeFetchError(
793
+ tooLarge ? `Decoded ${encoding} response body exceeded the size cap` : `Failed to decode ${encoding} response body`,
794
+ url,
795
+ tooLarge ? "response-too-large" : "decode-failed",
796
+ status
797
+ );
798
+ }
799
+ }
800
+ function errorCode(err) {
801
+ if (err != null && typeof err === "object" && "code" in err) {
802
+ const code = err.code;
803
+ return typeof code === "string" ? code : void 0;
804
+ }
805
+ return void 0;
806
+ }
807
+ function ipv4ToInt(ip) {
808
+ const parts = ip.split(".");
809
+ if (parts.length !== 4) return null;
810
+ let acc = 0;
811
+ for (const p of parts) {
812
+ const n = Number(p);
813
+ if (!Number.isInteger(n) || n < 0 || n > 255) return null;
814
+ acc = acc << 8 | n;
815
+ }
816
+ return acc >>> 0;
817
+ }
818
+ function isIpv4InCidr(intIp, cidr) {
819
+ const [addr, prefixRaw] = cidr.split("/");
820
+ const intCidr = ipv4ToInt(addr);
821
+ if (intCidr === null) return false;
822
+ const prefix = Number(prefixRaw);
823
+ if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
824
+ const mask = prefix === 0 ? 0 : -1 >>> 32 - prefix << 32 - prefix;
825
+ return (intIp & mask) >>> 0 === (intCidr & mask) >>> 0;
826
+ }
827
+ function isPublicIpv4(addr, allowLoopback) {
828
+ const intIp = ipv4ToInt(addr);
829
+ if (intIp === null) return false;
830
+ if (allowLoopback && addr === "127.0.0.1") return true;
831
+ for (const cidr of PRIVATE_IPV4_CIDRS) {
832
+ if (isIpv4InCidr(intIp, cidr)) return false;
833
+ }
834
+ return true;
835
+ }
836
+ function mappedIpv4(lower) {
837
+ const dotted = lower.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
838
+ if (dotted) return dotted[1];
839
+ const hex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
840
+ if (hex) {
841
+ const hi = parseInt(hex[1], 16);
842
+ const lo = parseInt(hex[2], 16);
843
+ return [hi >> 8, hi & 255, lo >> 8, lo & 255].join(".");
844
+ }
845
+ return null;
846
+ }
847
+ function isPublicIpv6(addr, allowLoopback) {
848
+ const lower = addr.toLowerCase();
849
+ if (allowLoopback && lower === "::1") return true;
850
+ const mapped = mappedIpv4(lower);
851
+ if (mapped) {
852
+ return isPublicIpv4(mapped, allowLoopback);
853
+ }
854
+ if (lower === "::" || lower === "::1") return false;
855
+ const firstHextet = lower.split(":")[0] || "";
856
+ if (firstHextet.startsWith("fc") || firstHextet.startsWith("fd"))
857
+ return false;
858
+ if (firstHextet.startsWith("fe8") || firstHextet.startsWith("fe9"))
859
+ return false;
860
+ if (firstHextet.startsWith("fea") || firstHextet.startsWith("feb"))
861
+ return false;
862
+ if (firstHextet.startsWith("ff")) return false;
863
+ return true;
864
+ }
865
+ async function fetchStoredBytes(url, context, label, options, signal) {
866
+ let response;
867
+ try {
868
+ response = await safeFetch(url, {
869
+ ...options?.maxBytes === void 0 ? {} : { maxResponseBytes: options.maxBytes },
870
+ /*
871
+ * A caller's signal REPLACES the deadline rather than joining it.
872
+ *
873
+ * These adapters look the object's address up before fetching it, and
874
+ * that lookup can stall too. Starting a fresh timer here would give the
875
+ * fetch its own full budget on top of however long the lookup took, so a
876
+ * read could outlive the deadline the caller was told applied — by
877
+ * roughly double. One signal begun before the lookup governs both phases.
878
+ */
879
+ ...signal !== void 0 ? { signal } : options?.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
880
+ });
881
+ } catch (error) {
882
+ const verdict = classifyFetchFailure(error);
883
+ if (verdict === "absent") return null;
884
+ if (verdict === "oversized") {
885
+ throw new StorageReadTooLargeError(
886
+ context,
887
+ resolveReadBounds(options).maxBytes
888
+ );
889
+ }
890
+ throw error;
891
+ }
892
+ if (response.status === 404) return null;
893
+ if (!response.ok) {
894
+ throw NextlyError.internal({
895
+ logContext: {
896
+ service: label,
897
+ path: context,
898
+ status: response.status,
899
+ statusText: response.statusText
900
+ }
901
+ });
902
+ }
903
+ return Buffer.from(await response.arrayBuffer());
904
+ }
905
+ function classifyFetchFailure(error) {
906
+ if (!(error instanceof SafeFetchError)) return "unknown";
907
+ if (error.status === 404) return "absent";
908
+ const successful = error.status !== void 0 && error.status >= 200 && error.status < 300;
909
+ return error.reason === "response-too-large" && successful ? "oversized" : "unknown";
910
+ }
911
+ function resolveReadBounds(options) {
912
+ return {
913
+ maxBytes: options?.maxBytes ?? DEFAULT_READ_MAX_BYTES,
914
+ timeoutMs: options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS
915
+ };
916
+ }
917
+ async function withDeadline(work, signal) {
918
+ if (signal === void 0) return await work;
919
+ return await Promise.race([
920
+ work,
921
+ new Promise((_, reject) => {
922
+ if (signal.aborted) {
923
+ reject(signal.reason);
924
+ return;
925
+ }
926
+ signal.addEventListener("abort", () => reject(signal.reason), {
927
+ once: true
928
+ });
929
+ })
930
+ ]);
931
+ }
932
+ var IPV4_RE, PRIVATE_IPV4_CIDRS, CLOUD_METADATA_HOSTS, ExternalUrlError, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_TIMEOUT_MS, SafeFetchError, DEFAULT_READ_TIMEOUT_MS, DEFAULT_READ_MAX_BYTES;
933
+ var init_chunk_C2VHLQKK = __esm({
934
+ "../nextly/dist/chunk-C2VHLQKK.mjs"() {
935
+ init_chunk_W77AQUUQ();
936
+ init_chunk_HJ5IQPOY();
937
+ IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
938
+ PRIVATE_IPV4_CIDRS = [
939
+ "0.0.0.0/8",
940
+ // RFC1122 "this network" / wildcard
941
+ "10.0.0.0/8",
942
+ // RFC1918
943
+ "100.64.0.0/10",
944
+ // RFC6598 CGNAT
945
+ "127.0.0.0/8",
946
+ // RFC1122 loopback
947
+ "169.254.0.0/16",
948
+ // RFC3927 link-local + AWS/GCP metadata
949
+ "172.16.0.0/12",
950
+ // RFC1918
951
+ "192.0.0.0/24",
952
+ // RFC6890 IETF protocol assignments
953
+ "192.0.2.0/24",
954
+ // RFC5737 documentation
955
+ "192.168.0.0/16",
956
+ // RFC1918
957
+ "198.18.0.0/15",
958
+ // RFC2544 benchmarking
959
+ "198.51.100.0/24",
960
+ // RFC5737 documentation
961
+ "203.0.113.0/24",
962
+ // RFC5737 documentation
963
+ "224.0.0.0/4",
964
+ // RFC5771 multicast
965
+ "240.0.0.0/4"
966
+ // RFC1112 reserved
967
+ ];
968
+ CLOUD_METADATA_HOSTS = /* @__PURE__ */ new Set([
969
+ "metadata.google.internal",
970
+ "metadata.googleapis.com",
971
+ "metadata"
972
+ // GCP short form
973
+ ]);
974
+ ExternalUrlError = class extends NextlyError {
975
+ constructor(message, url) {
976
+ super({
977
+ code: "EXTERNAL_URL_BLOCKED",
978
+ publicMessage: message,
979
+ logContext: { url }
980
+ });
981
+ this.url = url;
982
+ this.name = "ExternalUrlError";
983
+ }
984
+ };
985
+ DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
986
+ DEFAULT_TIMEOUT_MS = 3e4;
987
+ SafeFetchError = class extends NextlyError {
988
+ constructor(message, url, reason, status) {
989
+ super({
990
+ code: "EXTERNAL_REQUEST_FAILED",
991
+ publicMessage: message,
992
+ logContext: { url, reason }
993
+ });
994
+ this.url = url;
995
+ this.reason = reason;
996
+ this.status = status;
997
+ this.name = "SafeFetchError";
998
+ }
999
+ };
1000
+ DEFAULT_READ_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
1001
+ DEFAULT_READ_MAX_BYTES = DEFAULT_MAX_RESPONSE_BYTES;
1002
+ }
1003
+ });
1004
+
1005
+ // ../nextly/dist/chunk-IJCL4XOT.mjs
11
1006
  var BaseStorageAdapter;
12
- var init_chunk_2ROPMX7N = __esm({
13
- "../nextly/dist/chunk-2ROPMX7N.mjs"() {
1007
+ var init_chunk_IJCL4XOT = __esm({
1008
+ "../nextly/dist/chunk-IJCL4XOT.mjs"() {
1009
+ init_chunk_C2VHLQKK();
1010
+ init_chunk_W77AQUUQ();
14
1011
  BaseStorageAdapter = class {
15
1012
  /**
16
1013
  * Get adapter info including capabilities.
@@ -95,11 +1092,21 @@ var init_chunk_7P6ASYW6 = __esm({
95
1092
  }
96
1093
  });
97
1094
 
98
- // ../nextly/dist/chunk-VM3CQBI2.mjs
99
- init_chunk_2ROPMX7N();
1095
+ // ../nextly/dist/chunk-ANFXYTYN.mjs
1096
+ init_chunk_IJCL4XOT();
1097
+ module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
100
1098
 
101
1099
  // ../nextly/dist/storage/index.mjs
102
- init_chunk_2ROPMX7N();
1100
+ init_chunk_IJCL4XOT();
1101
+ init_chunk_C2VHLQKK();
1102
+ init_chunk_W77AQUUQ();
1103
+ init_chunk_HJ5IQPOY();
1104
+ init_chunk_7P6ASYW6();
1105
+
1106
+ // ../nextly/dist/storage/fetch-stored-bytes.mjs
1107
+ init_chunk_C2VHLQKK();
1108
+ init_chunk_W77AQUUQ();
1109
+ init_chunk_HJ5IQPOY();
103
1110
  init_chunk_7P6ASYW6();
104
1111
  var UploadthingStorageAdapter = class extends BaseStorageAdapter {
105
1112
  utapi;
@@ -178,6 +1185,37 @@ var UploadthingStorageAdapter = class extends BaseStorageAdapter {
178
1185
  return false;
179
1186
  }
180
1187
  }
1188
+ /**
1189
+ * Read a stored file back as bytes, or `null` when it is not there.
1190
+ *
1191
+ * A NETWORK round trip for the same reason as the Vercel adapter: the bytes
1192
+ * live on UploadThing's CDN. A caller serving these from its own origin has
1193
+ * to cache, or it pays the fetch on every request.
1194
+ *
1195
+ * The URL comes from `getFileUrls` rather than being assembled, because the
1196
+ * service owns the address and this adapter never chose it.
1197
+ *
1198
+ * @param filePath - File key
1199
+ * @returns The file's bytes, or `null` when no such key exists
1200
+ */
1201
+ async read(filePath, options) {
1202
+ const deadline = AbortSignal.timeout(
1203
+ options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS
1204
+ );
1205
+ const result = await withDeadline(
1206
+ this.utapi.getFileUrls([filePath], { keyType: "fileKey" }),
1207
+ deadline
1208
+ );
1209
+ const target = Array.from(result.data)[0]?.url;
1210
+ if (target === void 0) return null;
1211
+ return await fetchStoredBytes(
1212
+ target,
1213
+ filePath,
1214
+ "UploadThing",
1215
+ options,
1216
+ deadline
1217
+ );
1218
+ }
181
1219
  /**
182
1220
  * Get public URL for a file.
183
1221
  * Uploadthing files are served from utfs.io CDN.