@nextlyhq/storage-vercel-blob 0.0.2-alpha.6 → 0.0.2-alpha.60
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 +431 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +431 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,418 @@
|
|
|
2
2
|
|
|
3
3
|
var blob = require('@vercel/blob');
|
|
4
4
|
|
|
5
|
+
// src/adapter.ts
|
|
6
|
+
|
|
7
|
+
// ../nextly/dist/chunk-A6VGQLBN.mjs
|
|
8
|
+
function isDbError(err) {
|
|
9
|
+
if (!err || typeof err !== "object") return false;
|
|
10
|
+
const obj = err;
|
|
11
|
+
return obj.name === "DbError" && typeof obj.kind === "string";
|
|
12
|
+
}
|
|
13
|
+
var NEXTLY_ERROR_STATUS = {
|
|
14
|
+
VALIDATION_ERROR: 400,
|
|
15
|
+
INVALID_INPUT: 400,
|
|
16
|
+
AUTH_REQUIRED: 401,
|
|
17
|
+
AUTH_INVALID_CREDENTIALS: 401,
|
|
18
|
+
TOKEN_EXPIRED: 401,
|
|
19
|
+
FORBIDDEN: 403,
|
|
20
|
+
// The schema builder is off in this environment (production by default).
|
|
21
|
+
// Separate from FORBIDDEN: the caller's permissions are not the problem.
|
|
22
|
+
BUILDER_DISABLED: 403,
|
|
23
|
+
NOT_FOUND: 404,
|
|
24
|
+
CONFLICT: 409,
|
|
25
|
+
DUPLICATE: 409,
|
|
26
|
+
RATE_LIMITED: 429,
|
|
27
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
28
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
29
|
+
// 422: understood, well-formed, and refused on a rule the caller can act on.
|
|
30
|
+
// Deliberately NOT added to CANONICAL_CODE_FOR_STATUS -- that list drives the
|
|
31
|
+
// status -> code direction, where 422 stays mapped to INVALID_INPUT.
|
|
32
|
+
BUSINESS_RULE_VIOLATION: 422,
|
|
33
|
+
INTERNAL_ERROR: 500,
|
|
34
|
+
DATABASE_ERROR: 500,
|
|
35
|
+
EXTERNAL_SERVICE_ERROR: 502,
|
|
36
|
+
SERVICE_UNAVAILABLE: 503,
|
|
37
|
+
// Outbound-fetch safety (utils/validate-external-url): a URL refused for SSRF
|
|
38
|
+
// safety, and a fetch that timed out / exceeded the size cap / failed to decode.
|
|
39
|
+
EXTERNAL_URL_BLOCKED: 400,
|
|
40
|
+
EXTERNAL_REQUEST_FAILED: 502,
|
|
41
|
+
FILENAME_INVALID: 400,
|
|
42
|
+
EXTENSION_BLOCKED: 400,
|
|
43
|
+
MIME_BLOCKED: 415,
|
|
44
|
+
MIME_NOT_ALLOWED: 415,
|
|
45
|
+
SIZE_EXCEEDED: 413,
|
|
46
|
+
MAGIC_BYTE_MISMATCH: 400,
|
|
47
|
+
SVG_SANITIZATION_FAILED: 400,
|
|
48
|
+
UNSUPPORTED_FOR_BACKEND: 415,
|
|
49
|
+
// Plan B — schema bookkeeping consolidation.
|
|
50
|
+
NEXTLY_LEGACY_BOOKKEEPING_DETECTED: 409,
|
|
51
|
+
NEXTLY_UPGRADE_TABLE_NAME_COLLISION: 409,
|
|
52
|
+
NEXTLY_UPGRADE_IN_PROGRESS: 409,
|
|
53
|
+
// Plan C2 — nextly migrate phases.
|
|
54
|
+
NEXTLY_MIGRATE_LOCK_BUSY: 409,
|
|
55
|
+
NEXTLY_BASELINE_LOCK_NOT_HELD: 409,
|
|
56
|
+
NEXTLY_RESOLVE_LOCK_NOT_HELD: 409,
|
|
57
|
+
// Boot refused to serve: the migrate lock stayed held past the wait deadline,
|
|
58
|
+
// so this process never established whether the schema matches the code. 503
|
|
59
|
+
// rather than 409 — a load balancer should take the instance out of rotation
|
|
60
|
+
// and retry it, which is exactly the recovery this refusal wants.
|
|
61
|
+
NEXTLY_BOOT_MIGRATIONS_NOT_RUN: 503,
|
|
62
|
+
// Still running rather than refused. 503 for the same reason: retry shortly.
|
|
63
|
+
NEXTLY_BOOT_MIGRATIONS_PENDING: 503,
|
|
64
|
+
NEXTLY_CORE_DESTRUCTIVE_REFUSED: 409,
|
|
65
|
+
NEXTLY_MIGRATION_DRIFT: 409,
|
|
66
|
+
NEXTLY_MIGRATION_APPLY_FAILED: 500,
|
|
67
|
+
// Plan C3 — migrate:resolve recovery command.
|
|
68
|
+
NEXTLY_MIGRATION_FILE_MISSING: 404,
|
|
69
|
+
NEXTLY_MIGRATION_SNAPSHOT_MISSING: 404,
|
|
70
|
+
NEXTLY_MIGRATION_RESOLVE_DRIFT: 409,
|
|
71
|
+
NEXTLY_MIGRATION_RESOLVE_PRECONDITION: 409,
|
|
72
|
+
// Plan D — UI schema support.
|
|
73
|
+
NEXTLY_UI_SCHEMA_INVALID: 400,
|
|
74
|
+
NEXTLY_SCHEMA_SLUG_COLLISION: 409,
|
|
75
|
+
NEXTLY_SCHEMA_RELATION_TARGET_MISSING: 400,
|
|
76
|
+
// Plugin platform (P2b) — schema extend (contributes.extend) + relations (D15).
|
|
77
|
+
NEXTLY_SCHEMA_EXTEND_TARGET_UNKNOWN: 400,
|
|
78
|
+
NEXTLY_SCHEMA_EXTEND_FIELD_DUPLICATE: 409,
|
|
79
|
+
NEXTLY_SCHEMA_CROSS_PLUGIN_RELATION: 409,
|
|
80
|
+
// Plugin platform (P2c) — framework remap (.rename()).
|
|
81
|
+
NEXTLY_SCHEMA_RENAME_UNKNOWN_TARGET: 400,
|
|
82
|
+
// Plugin platform — a declared admin.clientConfig that cannot be delivered
|
|
83
|
+
// to the browser, refused at boot rather than serialized mangled.
|
|
84
|
+
NEXTLY_PLUGIN_CLIENT_CONFIG_INVALID: 500,
|
|
85
|
+
// Plugin platform (P0) — boot-time plugin dependency/version resolution.
|
|
86
|
+
PLUGIN_RESOLUTION_ERROR: 500,
|
|
87
|
+
// Plugin platform (P4) — contributes.routes collection (D25).
|
|
88
|
+
NEXTLY_ROUTE_COLLISION: 409,
|
|
89
|
+
NEXTLY_ROUTE_INVALID_PATH: 400
|
|
90
|
+
};
|
|
91
|
+
var CANONICAL_CODE_FOR_STATUS = [
|
|
92
|
+
"VALIDATION_ERROR",
|
|
93
|
+
"AUTH_REQUIRED",
|
|
94
|
+
"FORBIDDEN",
|
|
95
|
+
"NOT_FOUND",
|
|
96
|
+
"CONFLICT",
|
|
97
|
+
"PAYLOAD_TOO_LARGE",
|
|
98
|
+
"UNSUPPORTED_MEDIA_TYPE",
|
|
99
|
+
"RATE_LIMITED",
|
|
100
|
+
"EXTERNAL_SERVICE_ERROR",
|
|
101
|
+
"SERVICE_UNAVAILABLE"
|
|
102
|
+
];
|
|
103
|
+
({
|
|
104
|
+
...Object.fromEntries(
|
|
105
|
+
CANONICAL_CODE_FOR_STATUS.map((code) => [NEXTLY_ERROR_STATUS[code], code])
|
|
106
|
+
)});
|
|
107
|
+
var DB_ERROR_MAPPING = {
|
|
108
|
+
"unique-violation": {
|
|
109
|
+
code: "DUPLICATE",
|
|
110
|
+
statusCode: 409,
|
|
111
|
+
publicMessage: "Resource already exists."
|
|
112
|
+
},
|
|
113
|
+
"fk-violation": {
|
|
114
|
+
code: "VALIDATION_ERROR",
|
|
115
|
+
statusCode: 400,
|
|
116
|
+
publicMessage: "Referenced record does not exist."
|
|
117
|
+
},
|
|
118
|
+
"not-null-violation": {
|
|
119
|
+
code: "VALIDATION_ERROR",
|
|
120
|
+
statusCode: 400,
|
|
121
|
+
publicMessage: "A required field is missing."
|
|
122
|
+
},
|
|
123
|
+
constraint: {
|
|
124
|
+
code: "VALIDATION_ERROR",
|
|
125
|
+
statusCode: 400,
|
|
126
|
+
publicMessage: "The provided data violates a constraint."
|
|
127
|
+
},
|
|
128
|
+
deadlock: {
|
|
129
|
+
code: "CONFLICT",
|
|
130
|
+
statusCode: 409,
|
|
131
|
+
publicMessage: "The operation could not be completed. Please retry."
|
|
132
|
+
},
|
|
133
|
+
"serialization-failure": {
|
|
134
|
+
code: "CONFLICT",
|
|
135
|
+
statusCode: 409,
|
|
136
|
+
publicMessage: "The operation could not be completed. Please retry."
|
|
137
|
+
},
|
|
138
|
+
timeout: {
|
|
139
|
+
code: "DATABASE_ERROR",
|
|
140
|
+
statusCode: 500,
|
|
141
|
+
publicMessage: "The operation timed out. Please try again."
|
|
142
|
+
},
|
|
143
|
+
"connection-lost": {
|
|
144
|
+
code: "DATABASE_ERROR",
|
|
145
|
+
statusCode: 500,
|
|
146
|
+
publicMessage: "A temporary database error occurred. Please try again."
|
|
147
|
+
},
|
|
148
|
+
syntax: {
|
|
149
|
+
code: "INTERNAL_ERROR",
|
|
150
|
+
statusCode: 500,
|
|
151
|
+
publicMessage: "An unexpected error occurred."
|
|
152
|
+
},
|
|
153
|
+
internal: {
|
|
154
|
+
code: "INTERNAL_ERROR",
|
|
155
|
+
statusCode: 500,
|
|
156
|
+
publicMessage: "An unexpected error occurred."
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var NEXTLY_ERROR_BRAND = Symbol.for("nextly/NextlyError");
|
|
160
|
+
function hasBrand(value) {
|
|
161
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return value[NEXTLY_ERROR_BRAND] === true;
|
|
165
|
+
}
|
|
166
|
+
var NextlyError = class _NextlyError extends Error {
|
|
167
|
+
code;
|
|
168
|
+
statusCode;
|
|
169
|
+
publicMessage;
|
|
170
|
+
publicData;
|
|
171
|
+
messageKey;
|
|
172
|
+
logMessage;
|
|
173
|
+
logContext;
|
|
174
|
+
cause;
|
|
175
|
+
timestamp;
|
|
176
|
+
constructor(opts) {
|
|
177
|
+
super(opts.publicMessage);
|
|
178
|
+
this.name = "NextlyError";
|
|
179
|
+
this.code = opts.code;
|
|
180
|
+
this.publicMessage = opts.publicMessage;
|
|
181
|
+
this.publicData = opts.publicData;
|
|
182
|
+
this.messageKey = opts.messageKey;
|
|
183
|
+
this.logMessage = opts.logMessage;
|
|
184
|
+
this.logContext = opts.logContext;
|
|
185
|
+
this.cause = opts.cause;
|
|
186
|
+
this.timestamp = /* @__PURE__ */ new Date();
|
|
187
|
+
this.statusCode = _NextlyError.resolveStatusCode(opts);
|
|
188
|
+
Error.captureStackTrace?.(this, _NextlyError);
|
|
189
|
+
}
|
|
190
|
+
static {
|
|
191
|
+
_NextlyError.prototype[NEXTLY_ERROR_BRAND] = true;
|
|
192
|
+
}
|
|
193
|
+
static resolveStatusCode(opts) {
|
|
194
|
+
if (typeof opts.statusCode === "number") return opts.statusCode;
|
|
195
|
+
if (opts.code in NEXTLY_ERROR_STATUS) {
|
|
196
|
+
return NEXTLY_ERROR_STATUS[opts.code];
|
|
197
|
+
}
|
|
198
|
+
return 500;
|
|
199
|
+
}
|
|
200
|
+
/** HTTP-safe JSON. Strips logMessage / logContext / cause / stack. */
|
|
201
|
+
toResponseJSON(requestId) {
|
|
202
|
+
const json = {
|
|
203
|
+
code: String(this.code),
|
|
204
|
+
message: this.publicMessage,
|
|
205
|
+
requestId
|
|
206
|
+
};
|
|
207
|
+
if (this.messageKey) json.messageKey = this.messageKey;
|
|
208
|
+
if (this.publicData !== void 0) json.data = this.publicData;
|
|
209
|
+
return json;
|
|
210
|
+
}
|
|
211
|
+
/** Operator-facing JSON for log lines. Includes everything. */
|
|
212
|
+
toLogJSON(requestId) {
|
|
213
|
+
return {
|
|
214
|
+
code: this.code,
|
|
215
|
+
statusCode: this.statusCode,
|
|
216
|
+
publicMessage: this.publicMessage,
|
|
217
|
+
messageKey: this.messageKey,
|
|
218
|
+
logMessage: this.logMessage,
|
|
219
|
+
logContext: this.logContext,
|
|
220
|
+
cause: this.cause ? {
|
|
221
|
+
name: this.cause.name,
|
|
222
|
+
message: this.cause.message,
|
|
223
|
+
stack: this.cause.stack
|
|
224
|
+
} : void 0,
|
|
225
|
+
timestamp: this.timestamp.toISOString(),
|
|
226
|
+
requestId
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// ────────────────────────────────────────────────────────────────────
|
|
230
|
+
// Static factories — the recommended throw site for every common case.
|
|
231
|
+
// Public messages here follow the §13.8 rubric: complete sentence,
|
|
232
|
+
// generic, no identifiers, no value echoing, no policy hints.
|
|
233
|
+
// ────────────────────────────────────────────────────────────────────
|
|
234
|
+
static invalidCredentials(opts) {
|
|
235
|
+
return new _NextlyError({
|
|
236
|
+
code: "AUTH_INVALID_CREDENTIALS",
|
|
237
|
+
publicMessage: "Invalid email or password.",
|
|
238
|
+
logMessage: "Login failed",
|
|
239
|
+
logContext: opts?.logContext
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
static authRequired(opts) {
|
|
243
|
+
return new _NextlyError({
|
|
244
|
+
code: "AUTH_REQUIRED",
|
|
245
|
+
publicMessage: "Authentication required.",
|
|
246
|
+
logContext: opts?.logContext
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Distinct from `authRequired`: the caller was authenticated but the
|
|
251
|
+
* session token expired. Clients key on the TOKEN_EXPIRED *code* to
|
|
252
|
+
* silently refresh and retry rather than redirecting to login; the public
|
|
253
|
+
* message stays the generic spec §13.6 string ("Authentication required.")
|
|
254
|
+
* so the wire never reveals the session state — same as `authRequired`.
|
|
255
|
+
*/
|
|
256
|
+
static tokenExpired(opts) {
|
|
257
|
+
return new _NextlyError({
|
|
258
|
+
code: "TOKEN_EXPIRED",
|
|
259
|
+
publicMessage: "Authentication required.",
|
|
260
|
+
logContext: opts?.logContext
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
static notFound(opts) {
|
|
264
|
+
return new _NextlyError({
|
|
265
|
+
code: "NOT_FOUND",
|
|
266
|
+
publicMessage: "Not found.",
|
|
267
|
+
cause: opts?.cause,
|
|
268
|
+
logContext: opts?.logContext
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
static forbidden(opts) {
|
|
272
|
+
return new _NextlyError({
|
|
273
|
+
code: "FORBIDDEN",
|
|
274
|
+
publicMessage: "You don't have permission to perform this action.",
|
|
275
|
+
cause: opts?.cause,
|
|
276
|
+
logContext: opts?.logContext
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A call the caller got wrong, where naming the mistake IS the value of the
|
|
281
|
+
* error.
|
|
282
|
+
*
|
|
283
|
+
* The only factory that takes its public message from the caller. The
|
|
284
|
+
* generic messages elsewhere exist so an HTTP response cannot leak internal
|
|
285
|
+
* detail; this one is for arguments and configuration a developer controls
|
|
286
|
+
* and must be told about — a missing option, an unusable combination — where
|
|
287
|
+
* `internal()` would reduce the one useful sentence to "An unexpected error
|
|
288
|
+
* occurred." Do not pass user-supplied data through it.
|
|
289
|
+
*/
|
|
290
|
+
static invalidInput(opts) {
|
|
291
|
+
return new _NextlyError({
|
|
292
|
+
code: "INVALID_INPUT",
|
|
293
|
+
publicMessage: opts.message,
|
|
294
|
+
logContext: opts.logContext
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
static validation(opts) {
|
|
298
|
+
return new _NextlyError({
|
|
299
|
+
code: "VALIDATION_ERROR",
|
|
300
|
+
publicMessage: "Validation failed.",
|
|
301
|
+
publicData: { errors: opts.errors },
|
|
302
|
+
cause: opts.cause,
|
|
303
|
+
logContext: opts.logContext
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
static conflict(opts) {
|
|
307
|
+
return new _NextlyError({
|
|
308
|
+
code: "CONFLICT",
|
|
309
|
+
publicMessage: opts?.message ?? "The resource has changed since you last loaded it. Please refresh and try again.",
|
|
310
|
+
cause: opts?.cause,
|
|
311
|
+
logContext: { reason: opts?.reason, ...opts?.logContext }
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
static duplicate(opts) {
|
|
315
|
+
return new _NextlyError({
|
|
316
|
+
code: "DUPLICATE",
|
|
317
|
+
publicMessage: "Resource already exists.",
|
|
318
|
+
logContext: opts?.logContext
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
static rateLimited(opts) {
|
|
322
|
+
return new _NextlyError({
|
|
323
|
+
code: "RATE_LIMITED",
|
|
324
|
+
publicMessage: "Too many requests. Please try again later.",
|
|
325
|
+
publicData: opts?.retryAfterSeconds !== void 0 ? { retryAfterSeconds: opts.retryAfterSeconds } : void 0,
|
|
326
|
+
logContext: opts?.logContext
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
static internal(opts) {
|
|
330
|
+
return new _NextlyError({
|
|
331
|
+
code: "INTERNAL_ERROR",
|
|
332
|
+
publicMessage: "An unexpected error occurred.",
|
|
333
|
+
cause: opts?.cause,
|
|
334
|
+
logContext: opts?.logContext
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// Accepts an optional `logMessage` override so callers (e.g. the health
|
|
338
|
+
// route) can record a specific operator narrative ("Health check failed")
|
|
339
|
+
// while the public message stays canonical per spec §13.8.5.
|
|
340
|
+
static serviceUnavailable(opts) {
|
|
341
|
+
return new _NextlyError({
|
|
342
|
+
code: "SERVICE_UNAVAILABLE",
|
|
343
|
+
publicMessage: opts?.publicMessage ?? "Service unavailable. Please try again later.",
|
|
344
|
+
logMessage: opts?.logMessage,
|
|
345
|
+
cause: opts?.cause,
|
|
346
|
+
logContext: opts?.logContext
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Convert a DbError (or arbitrary unknown thrown by the DB layer) to a
|
|
351
|
+
* NextlyError with a generic public message and rich logContext. Used by
|
|
352
|
+
* `withDbErrors` for auto-conversion (Pattern A) and by services that
|
|
353
|
+
* catch DB errors at boundaries (Pattern B). Spec §8.2 mapping table.
|
|
354
|
+
*
|
|
355
|
+
* Never leaks DB driver text, constraint names, or table names into
|
|
356
|
+
* `publicMessage`. All DB context goes into `logContext`. The original
|
|
357
|
+
* DbError is preserved as `cause`.
|
|
358
|
+
*/
|
|
359
|
+
static fromDatabaseError(error) {
|
|
360
|
+
if (isDbError(error)) {
|
|
361
|
+
const mapping = DB_ERROR_MAPPING[error.kind];
|
|
362
|
+
const logContext = {
|
|
363
|
+
dbKind: error.kind,
|
|
364
|
+
dialect: error.dialect
|
|
365
|
+
};
|
|
366
|
+
if (error.code !== void 0) logContext.dbCode = error.code;
|
|
367
|
+
if (error.meta !== void 0) logContext.meta = error.meta;
|
|
368
|
+
return new _NextlyError({
|
|
369
|
+
code: mapping.code,
|
|
370
|
+
statusCode: mapping.statusCode,
|
|
371
|
+
publicMessage: mapping.publicMessage,
|
|
372
|
+
logMessage: "Database error",
|
|
373
|
+
logContext,
|
|
374
|
+
cause: error
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return new _NextlyError({
|
|
378
|
+
code: "INTERNAL_ERROR",
|
|
379
|
+
statusCode: 500,
|
|
380
|
+
publicMessage: "An unexpected error occurred.",
|
|
381
|
+
logMessage: "Non-DbError passed to fromDatabaseError",
|
|
382
|
+
cause: error instanceof Error ? error : void 0,
|
|
383
|
+
logContext: error instanceof Error ? void 0 : { value: String(error) }
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// ────────────────────────────────────────────────────────────────────
|
|
387
|
+
// Type guards. Structural rather than `instanceof` so they survive
|
|
388
|
+
// package-boundary mismatches (one consumer's NextlyError is a
|
|
389
|
+
// different module instance from another's).
|
|
390
|
+
// ────────────────────────────────────────────────────────────────────
|
|
391
|
+
static is(err) {
|
|
392
|
+
return hasBrand(err);
|
|
393
|
+
}
|
|
394
|
+
static isCode(err, code) {
|
|
395
|
+
return hasBrand(err) && err.code === code;
|
|
396
|
+
}
|
|
397
|
+
static isNotFound(err) {
|
|
398
|
+
return _NextlyError.isCode(err, "NOT_FOUND");
|
|
399
|
+
}
|
|
400
|
+
static isValidation(err) {
|
|
401
|
+
return _NextlyError.isCode(err, "VALIDATION_ERROR");
|
|
402
|
+
}
|
|
403
|
+
static isAuthRequired(err) {
|
|
404
|
+
return _NextlyError.isCode(err, "AUTH_REQUIRED");
|
|
405
|
+
}
|
|
406
|
+
static isForbidden(err) {
|
|
407
|
+
return _NextlyError.isCode(err, "FORBIDDEN");
|
|
408
|
+
}
|
|
409
|
+
static isConflict(err) {
|
|
410
|
+
return _NextlyError.isCode(err, "CONFLICT");
|
|
411
|
+
}
|
|
412
|
+
static isRateLimited(err) {
|
|
413
|
+
return _NextlyError.isCode(err, "RATE_LIMITED");
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
5
417
|
// src/adapter.ts
|
|
6
418
|
var VercelBlobStorageAdapter = class {
|
|
7
419
|
/**
|
|
@@ -47,14 +459,25 @@ var VercelBlobStorageAdapter = class {
|
|
|
47
459
|
const mimeType = (options.contentType || options.mimeType || "").toLowerCase().trim();
|
|
48
460
|
const filename = (options.filename || "").toLowerCase();
|
|
49
461
|
const ext = filename.includes(".") ? filename.slice(filename.lastIndexOf(".") + 1) : "";
|
|
50
|
-
const isSvg = mimeType === "image/svg+xml" || ext === "svg" || ext === "svgz";
|
|
51
462
|
const isHtml = mimeType === "text/html" || mimeType === "application/xhtml+xml" || ext === "html" || ext === "htm" || ext === "xhtml";
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
463
|
+
if (isHtml) {
|
|
464
|
+
throw NextlyError.validation({
|
|
465
|
+
errors: [
|
|
466
|
+
{
|
|
467
|
+
path: "file",
|
|
468
|
+
code: "UNSUPPORTED_FOR_BACKEND",
|
|
469
|
+
message: "HTML files cannot be hosted on Vercel Blob. Use the S3 or R2 adapter, or upload as another format."
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
logContext: {
|
|
473
|
+
adapter: "vercel-blob",
|
|
474
|
+
claimedMimeType: options.mimeType,
|
|
475
|
+
reason: "vercel-blob-rejects-html",
|
|
476
|
+
filename: options.filename
|
|
477
|
+
}
|
|
478
|
+
});
|
|
57
479
|
}
|
|
480
|
+
const isSvg = mimeType === "image/svg+xml" || ext === "svg" || ext === "svgz";
|
|
58
481
|
const pathname = this.buildPathname(options.filename, options.folder);
|
|
59
482
|
const result = await blob.put(pathname, buffer, {
|
|
60
483
|
access: this.resolvedConfig.access,
|
|
@@ -63,8 +486,9 @@ var VercelBlobStorageAdapter = class {
|
|
|
63
486
|
addRandomSuffix: this.resolvedConfig.addRandomSuffix,
|
|
64
487
|
cacheControlMaxAge: this.resolvedConfig.cacheControlMaxAge
|
|
65
488
|
});
|
|
489
|
+
const url = isSvg && options.contentDisposition === "attachment" ? result.downloadUrl : result.url;
|
|
66
490
|
return {
|
|
67
|
-
url
|
|
491
|
+
url,
|
|
68
492
|
path: result.url
|
|
69
493
|
};
|
|
70
494
|
}
|