@supacloud/lite 0.29.4 → 0.30.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.29.5...supacloud-lite-v0.30.0) (2026-10-17)
4
+
5
+
6
+ ### Features
7
+
8
+ * add configurable image-transformation input and output limits with a 500 MiB default and maximum ([#2027](https://github.com/vibeunion/supacloud/issues/2027)) ([5c07903](https://github.com/vibeunion/supacloud/commit/5c07903f20bfb069229cf3a829886fe96c78c288))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * enforce account eligibility consistently for bearer, email, OAuth and refresh authentication ([#2027](https://github.com/vibeunion/supacloud/issues/2027)) ([5c07903](https://github.com/vibeunion/supacloud/commit/5c07903f20bfb069229cf3a829886fe96c78c288))
14
+ * prepare isolated package artifacts against the current database dependency ([#2027](https://github.com/vibeunion/supacloud/issues/2027)) ([5c07903](https://github.com/vibeunion/supacloud/commit/5c07903f20bfb069229cf3a829886fe96c78c288))
15
+
16
+
17
+ ### Dependencies
18
+
19
+ * align the published @supacloud/db dependency to 0.18.1.
20
+
21
+ ## [0.29.5](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.29.4...supacloud-lite-v0.29.5) (2026-10-16)
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **lite:** reject ineligible password logins ([#1998](https://github.com/vibeunion/supacloud/issues/1998)) ([479e967](https://github.com/vibeunion/supacloud/commit/479e96719209f7aed26c44663d78e1442a7097f0))
27
+
3
28
  ## [0.29.4](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.29.3...supacloud-lite-v0.29.4) (2026-10-16)
4
29
 
5
30
 
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { dirname as dirname8, join as join10, resolve as resolve6 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.29.4",
11
+ version: "0.30.0",
12
12
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
@@ -67,16 +67,16 @@ var package_default = {
67
67
  "@electric-sql/pglite-pgmq": "0.0.8",
68
68
  "@electric-sql/pglite-pgvector": "0.0.11",
69
69
  "@electric-sql/pglite-postgis": "0.2.10",
70
- "@supacloud/db": "^0.17.0",
70
+ "@supacloud/db": "0.18.1",
71
71
  "exact-mirror": "1.2.6",
72
72
  tar: "^7.5.22",
73
73
  typebox: "1.3.37"
74
74
  },
75
75
  devDependencies: {
76
- "@supacloud/app": "file:../app",
77
- "@supacloud/contracts": "file:../contracts",
78
- "@supacloud/compiler": "file:../compiler",
79
- "@supacloud/elysia": "file:../elysia",
76
+ "@supacloud/app": "0.27.2",
77
+ "@supacloud/contracts": "0.11.2",
78
+ "@supacloud/compiler": "0.46.7",
79
+ "@supacloud/elysia": "0.33.2",
80
80
  "@supabase/supabase-js": "^2.117.3",
81
81
  "@types/bun": "^1.4.3",
82
82
  effect: "4.0.5",
@@ -85,13 +85,13 @@ var package_default = {
85
85
  typescript: "^7.0.2"
86
86
  },
87
87
  overrides: {
88
- "@supacloud/db": "^0.17.0",
89
- "@supacloud/app": "file:../app",
90
- "@supacloud/compiler": "file:../compiler",
91
- "@supacloud/elysia": "file:../elysia",
92
- "@supacloud/contracts": "file:../contracts",
93
- "@supacloud/commands": "file:../commands",
94
- "@supacloud/delivery": "file:../delivery",
88
+ "@supacloud/db": "0.18.1",
89
+ "@supacloud/app": "0.27.2",
90
+ "@supacloud/compiler": "0.46.7",
91
+ "@supacloud/elysia": "0.33.2",
92
+ "@supacloud/contracts": "0.11.2",
93
+ "@supacloud/commands": "0.9.4",
94
+ "@supacloud/delivery": "0.19.1",
95
95
  "@graphql-tools/utils": "^12.0.4"
96
96
  },
97
97
  engines: {
@@ -242,6 +242,13 @@ function isAllowedRedirect(requested, siteUrl, allowList = []) {
242
242
  }
243
243
 
244
244
  // src/runtime/auth/oauth.ts
245
+ class OAuthAccountIneligibleError extends Error {
246
+ code;
247
+ constructor(code) {
248
+ super(code);
249
+ this.code = code;
250
+ }
251
+ }
245
252
  var PRESETS = {
246
253
  google: {
247
254
  authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
@@ -364,14 +371,30 @@ class OAuthService {
364
371
  const msg = encodeURIComponent(e instanceof Error ? e.message : "oauth_error");
365
372
  return redirect(`${flow.redirect_to}#error=server_error&error_description=${msg}`);
366
373
  }
367
- const userId = await this.upsertUser(provider.name, profile);
374
+ let userId;
375
+ try {
376
+ userId = await this.upsertUser(provider.name, profile);
377
+ } catch (e) {
378
+ if (e instanceof OAuthAccountIneligibleError) {
379
+ return redirect(`${flow.redirect_to}#error=access_denied&error_code=${e.code}`);
380
+ }
381
+ throw e;
382
+ }
368
383
  if (flow.code_challenge) {
369
384
  const authCode = randomToken(24);
370
385
  await this.db.query(`insert into auth.flow_state (provider, provider_state, redirect_to, code_challenge, code_challenge_method, auth_code, user_id, expires_at)
371
386
  values ($1, $2, $3, $4, $5, $6, $7, now() + interval '5 minutes')`, [flow.provider, randomToken(16), flow.redirect_to, flow.code_challenge, flow.code_challenge_method, authCode, userId]);
372
387
  return redirect(`${flow.redirect_to}?code=${authCode}`);
373
388
  }
374
- const session = await createSession(userId);
389
+ let session;
390
+ try {
391
+ session = await createSession(userId);
392
+ } catch (e) {
393
+ if (e instanceof OAuthAccountIneligibleError) {
394
+ return redirect(`${flow.redirect_to}#error=access_denied&error_code=${e.code}`);
395
+ }
396
+ throw e;
397
+ }
375
398
  const hash = `#access_token=${session.access_token}&refresh_token=${session.refresh_token}&expires_in=${session.expires_in}&token_type=bearer&provider_token=`;
376
399
  return redirect(`${flow.redirect_to}${hash}`);
377
400
  }
@@ -422,31 +445,37 @@ class OAuthService {
422
445
  return profile;
423
446
  }
424
447
  async upsertUser(provider, profile) {
425
- const existing = await this.db.query(`select user_id from auth.identities where provider = $1 and provider_id = $2`, [provider, profile.id]);
448
+ return this.db.transaction((query) => this.upsertUserInTransaction(query, provider, profile));
449
+ }
450
+ async upsertUserInTransaction(query, provider, profile) {
451
+ const existing = await query(`select user_id from auth.identities where provider = $1 and provider_id = $2`, [provider, profile.id]);
426
452
  if (existing.rows[0]) {
427
- await this.db.query(`update auth.identities set last_sign_in_at = now(), identity_data = $3 where provider = $1 and provider_id = $2`, [
453
+ await this.assertUserEligible(query, existing.rows[0].user_id);
454
+ await query(`update auth.identities set last_sign_in_at = now(), identity_data = $3 where provider = $1 and provider_id = $2`, [
428
455
  provider,
429
456
  profile.id,
430
457
  JSON.stringify({ sub: profile.id, email: profile.email, ...profile.metadata })
431
458
  ]);
432
- await this.db.query(`update auth.users set last_sign_in_at = now() where id = $1`, [existing.rows[0].user_id]);
459
+ await query(`update auth.users set last_sign_in_at = now() where id = $1`, [existing.rows[0].user_id]);
433
460
  return existing.rows[0].user_id;
434
461
  }
435
462
  let userId;
436
463
  const email = profile.email?.toLowerCase() ?? null;
437
464
  const verified = !!profile.email && profile.emailVerified === true;
438
465
  if (email && verified) {
439
- const byEmail = await this.db.query(`select id from auth.users where email = $1`, [email]);
466
+ const byEmail = await query(`select id from auth.users where email = $1`, [email]);
440
467
  userId = byEmail.rows[0]?.id;
468
+ if (userId)
469
+ await this.assertUserEligible(query, userId);
441
470
  }
442
471
  if (!userId) {
443
472
  let userEmail = verified ? email : null;
444
473
  if (userEmail) {
445
- const taken = await this.db.query(`select 1 from auth.users where email = $1`, [userEmail]);
474
+ const taken = await query(`select 1 from auth.users where email = $1`, [userEmail]);
446
475
  if (taken.rows.length > 0)
447
476
  userEmail = null;
448
477
  }
449
- const created = await this.db.query(`insert into auth.users (aud, role, email, email_confirmed_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data)
478
+ const created = await query(`insert into auth.users (aud, role, email, email_confirmed_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data)
450
479
  values ('authenticated','authenticated',$1, $2, now(),
451
480
  $3::jsonb, $4::jsonb) returning id`, [
452
481
  userEmail,
@@ -459,12 +488,22 @@ class OAuthService {
459
488
  throw new Error("OAuth user creation returned no row");
460
489
  userId = user.id;
461
490
  }
462
- await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
491
+ await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
463
492
  values ($1, $2, $3, $4::jsonb)`, [userId, provider, profile.id, JSON.stringify({ sub: profile.id, email: profile.email, ...profile.metadata })]);
464
493
  if (!userId)
465
494
  throw new Error("OAuth identity has no user ID");
466
495
  return userId;
467
496
  }
497
+ async assertUserEligible(query, userId) {
498
+ const users = await query(`select deleted_at is not null as is_deleted,
499
+ coalesce(banned_until > now(), false) as is_banned
500
+ from auth.users where id = $1 for update`, [userId]);
501
+ const user = users.rows[0];
502
+ if (!user || user.is_deleted)
503
+ throw new OAuthAccountIneligibleError("user_not_found");
504
+ if (user.is_banned)
505
+ throw new OAuthAccountIneligibleError("user_banned");
506
+ }
468
507
  }
469
508
  function redirect(location) {
470
509
  return new Response(null, { status: 303, headers: { location } });
@@ -1350,6 +1389,8 @@ class AuthHandler {
1350
1389
  return await this.admin(req, ctx, path, method);
1351
1390
  return authError(404, "not_found", `unknown auth endpoint: ${path}`);
1352
1391
  } catch (e) {
1392
+ if (e instanceof OAuthAccountIneligibleError)
1393
+ return authError(400, e.code, e.message);
1353
1394
  const msg = e instanceof Error ? e.message : String(e);
1354
1395
  return authError(500, "unexpected_failure", msg);
1355
1396
  }
@@ -1413,8 +1454,12 @@ class AuthHandler {
1413
1454
  const body = await req.json().catch(() => ({}));
1414
1455
  if (grantType === "password") {
1415
1456
  const email = (body.email ?? "").toLowerCase().trim();
1416
- const res = await this.db.query(`select * from auth.users where email = $1`, [email]);
1457
+ const res = await this.db.query(`select *, coalesce(banned_until > now(), false) as is_banned
1458
+ from auth.users where email = $1 and deleted_at is null`, [email]);
1417
1459
  const user = res.rows[0];
1460
+ if (user?.encrypted_password && user.is_banned) {
1461
+ return authError(400, "user_banned", "User is banned");
1462
+ }
1418
1463
  if (!user || !user.encrypted_password || !await verifyPassword(body.password ?? "", user.encrypted_password)) {
1419
1464
  await this.audit("login_failed", { actorEmail: email, traits: { grant_type: "password" } });
1420
1465
  return authError(400, "invalid_credentials", "Invalid login credentials");
@@ -1440,8 +1485,7 @@ class AuthHandler {
1440
1485
  const userId = await this.oauth.exchangePkce(authCode, verifier);
1441
1486
  if (!userId)
1442
1487
  return authError(403, "flow_state_not_found", "invalid or expired auth code");
1443
- const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
1444
- return json(200, await this.sessionForOAuth(ures.rows[0]));
1488
+ return json(200, await this.sessionForOAuth(userId));
1445
1489
  }
1446
1490
  return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
1447
1491
  }
@@ -1736,7 +1780,10 @@ Or sign in with this link: ${link}`
1736
1780
  tokenTypes: `{${types.join(",")}}`,
1737
1781
  email: email?.toLowerCase().trim() ?? null
1738
1782
  };
1739
- return this.db.transaction((query) => this.claimEmailToken(query, redemption));
1783
+ return this.db.transaction(async (query) => {
1784
+ const user = await this.claimEmailToken(query, redemption);
1785
+ return user ? this.sessionFor(user, undefined, undefined, query) : null;
1786
+ });
1740
1787
  }
1741
1788
  async claimEmailToken(query, redemption) {
1742
1789
  const candidate = await this.findEmailTokenCandidate(query, redemption);
@@ -1748,7 +1795,10 @@ Or sign in with this link: ${link}`
1748
1795
  return claimedToken ? this.confirmEmailTokenUser(query, claimedToken) : null;
1749
1796
  }
1750
1797
  async lockAndDeleteEmailToken(query, candidate, redemption) {
1751
- await query(`select id from auth.users where id = $1 for update`, [candidate.user_id]);
1798
+ const eligible = await query(`select id from auth.users where id = $1 and deleted_at is null
1799
+ and (banned_until is null or banned_until <= now()) for update`, [candidate.user_id]);
1800
+ if (eligible.rows.length === 0)
1801
+ return null;
1752
1802
  const claimedTokens = await query(`delete from auth.one_time_tokens
1753
1803
  where id = $1 and token = $2 and token_type = any($3::text[])
1754
1804
  and expires_at > now() and attempts < $4
@@ -1801,10 +1851,10 @@ Or sign in with this link: ${link}`
1801
1851
  return json(200, session);
1802
1852
  }
1803
1853
  const types = body.type === "recovery" ? ["recovery"] : body.type === "magiclink" ? ["magiclink"] : ["otp", "magiclink"];
1804
- const user = await this.redeem(token, types, body.email);
1805
- if (!user)
1854
+ const session = await this.redeem(token, types, body.email);
1855
+ if (!session)
1806
1856
  return authError(403, "otp_expired", "Token has expired or is invalid");
1807
- return json(200, await this.sessionFor(user));
1857
+ return json(200, session);
1808
1858
  }
1809
1859
  async redeemPhoneOtp(phone, code) {
1810
1860
  const tokenDigest = `hmac-sha256:v1:${await keyedDigest(this.config.jwtSecret, "supacloud-lite:phone-otp:v1", `${phone}\x00${code}`)}`;
@@ -1845,11 +1895,10 @@ Or sign in with this link: ${link}`
1845
1895
  const token = url.searchParams.get("token") ?? "";
1846
1896
  const type = url.searchParams.get("type") ?? "magiclink";
1847
1897
  const redirectTo = resolveRedirect(url.searchParams.get("redirect_to"), this.config.siteUrl, this.config.uriAllowList, this.config.enforceRedirectAllowList);
1848
- const user = await this.redeem(token, [type]);
1849
- if (!user) {
1898
+ const session = await this.redeem(token, [type]);
1899
+ if (!session) {
1850
1900
  return new Response(null, { status: 303, headers: { location: `${redirectTo}#error=access_denied&error_code=otp_expired` } });
1851
1901
  }
1852
- const session = await this.sessionFor(user);
1853
1902
  const hash = `#access_token=${session.access_token}&refresh_token=${session.refresh_token}&expires_in=${session.expires_in}&token_type=bearer&type=${type}`;
1854
1903
  return new Response(null, { status: 303, headers: { location: `${redirectTo}${hash}` } });
1855
1904
  }
@@ -2150,6 +2199,11 @@ Or sign in with this link: ${link}`
2150
2199
  return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
2151
2200
  }
2152
2201
  const session = await this.db.transaction(async (query) => {
2202
+ const currentUsers = await query(`select * from auth.users where id = $1 and deleted_at is null
2203
+ and (banned_until is null or banned_until <= now()) for update`, [user.id]);
2204
+ const currentUser = currentUsers.rows[0];
2205
+ if (!currentUser)
2206
+ return null;
2153
2207
  const claimed = await query(`update auth.mfa_challenges set verified_at = now()
2154
2208
  where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
2155
2209
  returning id`, [challenge.id, factorId]);
@@ -2158,7 +2212,7 @@ Or sign in with this link: ${link}`
2158
2212
  if (factor.status !== "verified") {
2159
2213
  await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2160
2214
  }
2161
- return this.sessionFor(user, undefined, {
2215
+ return this.sessionFor(currentUser, undefined, {
2162
2216
  aal: "aal2",
2163
2217
  amr: [
2164
2218
  { method: "password", timestamp: Math.floor(Date.now() / 1000) },
@@ -2219,6 +2273,18 @@ Or sign in with this link: ${link}`
2219
2273
  }
2220
2274
  async rotateRefreshToken(token) {
2221
2275
  return this.db.transaction(async (query) => {
2276
+ const availableToken = await this.findAvailableRefreshToken(query, token);
2277
+ if (!availableToken)
2278
+ return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
2279
+ const userResult = await query(`select *, coalesce(banned_until > now(), false) as is_banned
2280
+ from auth.users where id = $1 for update`, [availableToken.user_id]);
2281
+ const user = userResult.rows[0];
2282
+ if (!user || user.deleted_at !== null) {
2283
+ return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
2284
+ }
2285
+ if (user.is_banned) {
2286
+ return authError(400, "user_banned", "Invalid Refresh Token: User Banned");
2287
+ }
2222
2288
  const claimedToken = await this.claimRefreshToken(query, token);
2223
2289
  if (!claimedToken)
2224
2290
  return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
@@ -2226,14 +2292,19 @@ Or sign in with this link: ${link}`
2226
2292
  const expiryError = this.refreshExpiryError(claimedToken, sessionStartedAt);
2227
2293
  if (expiryError)
2228
2294
  return expiryError;
2229
- const userResult = await query(`select * from auth.users where id = $1`, [claimedToken.user_id]);
2230
- const session = await this.sessionFor(userResult.rows[0], token, {
2295
+ const session = await this.sessionFor(user, token, {
2231
2296
  sessionId: claimedToken.session_id ?? undefined,
2232
2297
  sessionStartedAt: sessionStartedAt === null ? undefined : Math.floor(sessionStartedAt / 1000)
2233
2298
  }, query);
2234
2299
  return json(200, session);
2235
2300
  });
2236
2301
  }
2302
+ async findAvailableRefreshToken(query, token) {
2303
+ const available = await query(`select user_id, session_id, created_at
2304
+ from auth.refresh_tokens
2305
+ where token = $1 and revoked is not true`, [token]);
2306
+ return available.rows[0];
2307
+ }
2237
2308
  async claimRefreshToken(query, token) {
2238
2309
  const claimed = await query(`update auth.refresh_tokens set revoked = true, updated_at = now()
2239
2310
  where token = $1 and revoked is not true
@@ -2261,7 +2332,8 @@ Or sign in with this link: ${link}`
2261
2332
  const claims = await this.claimsFromBearer(req);
2262
2333
  if (!claims?.sub)
2263
2334
  return null;
2264
- const res = await this.db.query(`select * from auth.users where id = $1`, [claims.sub]);
2335
+ const res = await this.db.query(`select * from auth.users where id = $1 and deleted_at is null
2336
+ and (banned_until is null or banned_until <= now())`, [claims.sub]);
2265
2337
  return res.rows[0] ?? null;
2266
2338
  }
2267
2339
  async claimsFromBearer(req) {
@@ -2290,14 +2362,22 @@ Or sign in with this link: ${link}`
2290
2362
  is_anonymous: u.is_anonymous ?? false
2291
2363
  };
2292
2364
  }
2293
- sessionForOAuth(user) {
2294
- return this.sessionFor(user, undefined, {
2295
- amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
2365
+ sessionForOAuth(userId) {
2366
+ return this.db.transaction(async (query) => {
2367
+ const res = await query(`select *, coalesce(banned_until > now(), false) as is_banned
2368
+ from auth.users where id = $1 for update`, [userId]);
2369
+ const user = res.rows[0];
2370
+ if (!user || user.deleted_at !== null)
2371
+ throw new OAuthAccountIneligibleError("user_not_found");
2372
+ if (user.is_banned)
2373
+ throw new OAuthAccountIneligibleError("user_banned");
2374
+ return this.sessionFor(user, undefined, {
2375
+ amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
2376
+ }, query);
2296
2377
  });
2297
2378
  }
2298
2379
  async oauthSessionTokensFor(userId) {
2299
- const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
2300
- const session = await this.sessionForOAuth(res.rows[0]);
2380
+ const session = await this.sessionForOAuth(userId);
2301
2381
  return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
2302
2382
  }
2303
2383
  async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
@@ -8974,8 +9054,18 @@ var IMAGE_FORMAT_CONTENT_TYPES = {
8974
9054
  gif: "image/gif"
8975
9055
  };
8976
9056
  var MAX_TRANSFORM_DIMENSION = 2500;
8977
- var MAX_TRANSFORM_BYTES = 25 * 1024 * 1024;
9057
+ var MAX_TRANSFORM_BYTES = 500 * 1024 * 1024;
8978
9058
  var MAX_TRANSFORM_PIXELS = 50000000;
9059
+ function resolveImageTransformLimits(options = {}) {
9060
+ const maxInputBytes = options.maxInputBytes === undefined ? MAX_TRANSFORM_BYTES : options.maxInputBytes;
9061
+ const maxOutputBytes = options.maxOutputBytes === undefined ? MAX_TRANSFORM_BYTES : options.maxOutputBytes;
9062
+ for (const [name, value] of Object.entries({ maxInputBytes, maxOutputBytes })) {
9063
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_TRANSFORM_BYTES) {
9064
+ throw new Error(`Invalid image transformation ${name}: expected 1..${MAX_TRANSFORM_BYTES} bytes`);
9065
+ }
9066
+ }
9067
+ return { maxInputBytes, maxOutputBytes };
9068
+ }
8979
9069
  function invalid(message) {
8980
9070
  return { ok: false, status: 400, error: "InvalidTransformation", message };
8981
9071
  }
@@ -9033,7 +9123,16 @@ function parseImageTransform(query) {
9033
9123
  if (quality !== undefined && format === "png") {
9034
9124
  return invalid("quality is supported only for jpeg and webp output");
9035
9125
  }
9036
- return { ok: true, value: { width, height, resize, quality, format } };
9126
+ return {
9127
+ ok: true,
9128
+ value: {
9129
+ resize,
9130
+ format,
9131
+ ...width === undefined ? {} : { width },
9132
+ ...height === undefined ? {} : { height },
9133
+ ...quality === undefined ? {} : { quality }
9134
+ }
9135
+ };
9037
9136
  }
9038
9137
  function imageTransformToSearchParams(options) {
9039
9138
  const query = new URLSearchParams;
@@ -9107,15 +9206,16 @@ function applyResize(image, metadata, options) {
9107
9206
  const proportionalWidth = Math.max(1, Math.round(metadata.width * height / metadata.height));
9108
9207
  image.resize(proportionalWidth);
9109
9208
  }
9110
- async function transformImage(source, options, knownSourceSize) {
9209
+ async function transformImage(source, options, knownSourceSize, configuredLimits = {}) {
9210
+ const limits = resolveImageTransformLimits(configuredLimits);
9111
9211
  const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
9112
9212
  const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
9113
- if (sourceSize > MAX_TRANSFORM_BYTES) {
9213
+ if (sourceSize > limits.maxInputBytes) {
9114
9214
  return {
9115
9215
  ok: false,
9116
9216
  status: 413,
9117
9217
  error: "ImageTooLarge",
9118
- message: "The source image exceeds the 25MB transformation limit"
9218
+ message: `The source image exceeds the transformation limit (${limits.maxInputBytes} bytes)`
9119
9219
  };
9120
9220
  }
9121
9221
  const image = new Bun.Image(source, { maxPixels: MAX_TRANSFORM_PIXELS });
@@ -9148,12 +9248,12 @@ async function transformImage(source, options, knownSourceSize) {
9148
9248
  }
9149
9249
  try {
9150
9250
  const bytes = await image.bytes();
9151
- if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
9251
+ if (bytes.byteLength > limits.maxOutputBytes) {
9152
9252
  return {
9153
9253
  ok: false,
9154
9254
  status: 413,
9155
9255
  error: "ImageTooLarge",
9156
- message: "The transformed image exceeds the 25MB transformation limit"
9256
+ message: `The transformed image exceeds the transformation limit (${limits.maxOutputBytes} bytes)`
9157
9257
  };
9158
9258
  }
9159
9259
  return { ok: true, bytes, contentType };
@@ -9356,10 +9456,12 @@ class StorageHandler {
9356
9456
  tusUploads = new Map;
9357
9457
  mutationTail = Promise.resolve();
9358
9458
  imageTransforms = new ImageTransformCache;
9459
+ transformLimits;
9359
9460
  constructor(db, driver, config) {
9360
9461
  this.db = db;
9361
9462
  this.driver = driver;
9362
9463
  this.config = config;
9464
+ this.transformLimits = resolveImageTransformLimits(config.imageTransformLimits);
9363
9465
  }
9364
9466
  async ensureBuckets(buckets) {
9365
9467
  for (const b of buckets) {
@@ -9635,7 +9737,7 @@ class StorageHandler {
9635
9737
  const source = await this.readObjectSource(row);
9636
9738
  if (source === null)
9637
9739
  throw new StorageObjectMissingError;
9638
- return transformImage(source, parsed.value, objectSize(row));
9740
+ return transformImage(source, parsed.value, objectSize(row), this.transformLimits);
9639
9741
  });
9640
9742
  } catch (error) {
9641
9743
  if (error instanceof StorageObjectMissingError) {
@@ -11731,6 +11833,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
11731
11833
  const storage = new StorageHandler(db, config.storageDriver ?? new MemoryStorageDriver, {
11732
11834
  jwtSecret,
11733
11835
  defaultFileSizeLimit: config.storageFileSizeLimit,
11836
+ imageTransformLimits: config.storageImageTransformLimits,
11734
11837
  log
11735
11838
  });
11736
11839
  if (config.buckets?.length) {
@@ -13270,8 +13373,22 @@ import { readFile as readFile2, stat } from "fs/promises";
13270
13373
  import { dirname as dirname3, resolve as resolve3 } from "path";
13271
13374
  import { pathToFileURL } from "url";
13272
13375
 
13273
- // node_modules/@supacloud/db/dist/index.js
13376
+ // ../db/dist/index.js
13274
13377
  import { createHash as createHash3 } from "crypto";
13378
+
13379
+ // ../contracts/dist/index.mjs
13380
+ var commandErrorBrand = Symbol.for("@supacloud/contracts/CommandError/v1");
13381
+ var commandErrorCodes = new Set([
13382
+ "COMMAND_INPUT_INVALID",
13383
+ "COMMAND_REJECTED",
13384
+ "COMMAND_UNAVAILABLE",
13385
+ "COMMAND_IDEMPOTENCY_CONFLICT",
13386
+ "COMMAND_RECEIPT_INVALID",
13387
+ "COMMAND_OUTCOME_UNKNOWN",
13388
+ "COMMAND_INPUT_EXPIRED"
13389
+ ]);
13390
+
13391
+ // ../db/dist/index.js
13275
13392
  var TABLES_SQL = `
13276
13393
  SELECT n.nspname AS schema,
13277
13394
  c.relname AS name,
@@ -14680,6 +14797,19 @@ function readStorage(root) {
14680
14797
  const limit = getByteSize(storage, "file_size_limit");
14681
14798
  if (limit !== undefined)
14682
14799
  out.fileSizeLimit = limit;
14800
+ const image = tableAt(root, "storage.image_transformation");
14801
+ if (image) {
14802
+ const limits = {};
14803
+ for (const [key, field] of [["max_input_bytes", "maxInputBytes"], ["max_output_bytes", "maxOutputBytes"]]) {
14804
+ if (!image.values.has(key))
14805
+ continue;
14806
+ const value = getByteSize(image, key);
14807
+ if (value === undefined)
14808
+ throw new Error(`Invalid image transformation ${key}`);
14809
+ limits[field] = value;
14810
+ }
14811
+ out.imageTransformLimits = resolveImageTransformLimits(limits);
14812
+ }
14683
14813
  const buckets = tableAt(root, "storage.buckets");
14684
14814
  if (buckets && buckets.children.size > 0) {
14685
14815
  out.buckets = [...buckets.children].map(([id, t]) => ({
@@ -15517,6 +15647,7 @@ async function createProjectBackend(options = {}) {
15517
15647
  dbSchemas: config.api.schemas,
15518
15648
  maxRows: config.api.maxRows,
15519
15649
  storageFileSizeLimit: config.storage.fileSizeLimit,
15650
+ storageImageTransformLimits: config.storage.imageTransformLimits,
15520
15651
  buckets: config.storage.buckets,
15521
15652
  migrations: options.applyMigrations === false ? [] : project.migrations,
15522
15653
  seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,