@supacloud/lite 0.31.2 → 0.32.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/dist/index.js CHANGED
@@ -88,7 +88,7 @@ function randomToken(bytes = 32) {
88
88
  // package.json
89
89
  var package_default = {
90
90
  name: "@supacloud/lite",
91
- version: "0.31.2",
91
+ version: "0.32.0",
92
92
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
93
93
  type: "module",
94
94
  license: "Apache-2.0",
@@ -147,16 +147,16 @@ var package_default = {
147
147
  "@electric-sql/pglite-pgmq": "0.0.8",
148
148
  "@electric-sql/pglite-pgvector": "0.0.11",
149
149
  "@electric-sql/pglite-postgis": "0.2.10",
150
- "@supacloud/db": "0.18.3",
150
+ "@supacloud/db": "0.19.0",
151
151
  "exact-mirror": "1.2.6",
152
152
  tar: "^7.5.22",
153
153
  typebox: "1.3.37"
154
154
  },
155
155
  devDependencies: {
156
- "@supacloud/app": "0.28.2",
157
- "@supacloud/contracts": "0.11.4",
158
- "@supacloud/compiler": "0.47.2",
159
- "@supacloud/elysia": "0.33.4",
156
+ "@supacloud/app": "0.29.0",
157
+ "@supacloud/contracts": "0.12.0",
158
+ "@supacloud/compiler": "0.48.0",
159
+ "@supacloud/elysia": "0.34.0",
160
160
  "@supabase/supabase-js": "^2.117.3",
161
161
  "@types/bun": "^1.4.3",
162
162
  effect: "4.0.5",
@@ -165,13 +165,13 @@ var package_default = {
165
165
  typescript: "^7.0.2"
166
166
  },
167
167
  overrides: {
168
- "@supacloud/db": "0.18.3",
169
- "@supacloud/app": "0.28.2",
170
- "@supacloud/compiler": "0.47.2",
171
- "@supacloud/elysia": "0.33.4",
172
- "@supacloud/contracts": "0.11.4",
173
- "@supacloud/commands": "0.9.6",
174
- "@supacloud/delivery": "0.19.3",
168
+ "@supacloud/db": "0.19.0",
169
+ "@supacloud/app": "0.29.0",
170
+ "@supacloud/compiler": "0.48.0",
171
+ "@supacloud/elysia": "0.34.0",
172
+ "@supacloud/contracts": "0.12.0",
173
+ "@supacloud/commands": "0.10.0",
174
+ "@supacloud/delivery": "0.20.0",
175
175
  "@graphql-tools/utils": "^12.0.4"
176
176
  },
177
177
  engines: {
@@ -373,9 +373,8 @@ class OAuthService {
373
373
  try {
374
374
  const token = await this.exchangeCode(provider, code);
375
375
  profile = await this.fetchProfile(provider, token);
376
- } catch (e) {
377
- const msg = encodeURIComponent(e instanceof Error ? e.message : "oauth_error");
378
- return redirect(`${flow.redirect_to}#error=server_error&error_description=${msg}`);
376
+ } catch {
377
+ return redirect(`${flow.redirect_to}#error=server_error&error_description=${encodeURIComponent("OAuth provider request failed")}`);
379
378
  }
380
379
  let userId;
381
380
  try {
@@ -384,7 +383,7 @@ class OAuthService {
384
383
  if (e instanceof OAuthAccountIneligibleError) {
385
384
  return redirect(`${flow.redirect_to}#error=access_denied&error_code=${e.code}`);
386
385
  }
387
- throw e;
386
+ return redirect(`${flow.redirect_to}#error=server_error&error_description=${encodeURIComponent("OAuth callback failed")}`);
388
387
  }
389
388
  if (flow.code_challenge) {
390
389
  const authCode = randomToken(24);
@@ -399,13 +398,13 @@ class OAuthService {
399
398
  if (e instanceof OAuthAccountIneligibleError) {
400
399
  return redirect(`${flow.redirect_to}#error=access_denied&error_code=${e.code}`);
401
400
  }
402
- throw e;
401
+ return redirect(`${flow.redirect_to}#error=server_error&error_description=${encodeURIComponent("OAuth callback failed")}`);
403
402
  }
404
403
  const hash = `#access_token=${session.access_token}&refresh_token=${session.refresh_token}&expires_in=${session.expires_in}&token_type=bearer&provider_token=`;
405
404
  return redirect(`${flow.redirect_to}${hash}`);
406
405
  }
407
406
  async exchangePkce(authCode, verifier) {
408
- const res = await this.db.query(`delete from auth.flow_state where auth_code = $1 and expires_at > now() returning *`, [authCode]);
407
+ const res = await this.db.query(`select * from auth.flow_state where auth_code = $1 and expires_at > now()`, [authCode]);
409
408
  const flow = res.rows[0];
410
409
  if (!flow || !flow.user_id)
411
410
  return null;
@@ -417,7 +416,11 @@ class OAuthService {
417
416
  } else if (flow.code_challenge && flow.code_challenge !== verifier) {
418
417
  return null;
419
418
  }
420
- return flow.user_id;
419
+ const claimed = await this.db.transaction(async (query) => {
420
+ const consumed = await query(`delete from auth.flow_state where auth_code = $1 and expires_at > now() returning *`, [authCode]);
421
+ return consumed.rows[0] ?? null;
422
+ });
423
+ return claimed?.user_id ?? null;
421
424
  }
422
425
  async exchangeCode(provider, code) {
423
426
  const body = new URLSearchParams({
@@ -466,39 +469,65 @@ class OAuthService {
466
469
  return existing.rows[0].user_id;
467
470
  }
468
471
  let userId;
469
- const email = profile.email?.toLowerCase() ?? null;
472
+ const email = profile.email?.toLowerCase().trim() ?? null;
470
473
  const verified = !!profile.email && profile.emailVerified === true;
471
474
  if (email && verified) {
472
- const byEmail = await query(`select id from auth.users where email = $1`, [email]);
475
+ const byEmail = await query(`select id from auth.users where lower(email) = $1`, [email]);
473
476
  userId = byEmail.rows[0]?.id;
474
477
  if (userId)
475
478
  await this.assertUserEligible(query, userId);
476
479
  }
480
+ let createdUserId;
477
481
  if (!userId) {
478
482
  let userEmail = verified ? email : null;
479
483
  if (userEmail) {
480
- const taken = await query(`select 1 from auth.users where email = $1`, [userEmail]);
484
+ const taken = await query(`select 1 from auth.users where lower(email) = $1`, [userEmail]);
481
485
  if (taken.rows.length > 0)
482
486
  userEmail = null;
483
487
  }
484
488
  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)
485
489
  values ('authenticated','authenticated',$1, $2, now(),
486
- $3::jsonb, $4::jsonb) returning id`, [
490
+ $3::jsonb, $4::jsonb)
491
+ on conflict (email) do nothing
492
+ returning id`, [
487
493
  userEmail,
488
494
  userEmail ? new Date().toISOString() : null,
489
495
  JSON.stringify({ provider, providers: [provider] }),
490
496
  JSON.stringify({ sub: profile.id, email: profile.email, full_name: profile.name, ...profile.metadata })
491
497
  ]);
492
498
  const user = created.rows[0];
493
- if (!user)
499
+ if (user) {
500
+ userId = user.id;
501
+ createdUserId = user.id;
502
+ } else if (userEmail) {
503
+ const existingEmail = await query(`select id from auth.users where lower(email) = $1 for update`, [userEmail]);
504
+ userId = existingEmail.rows[0]?.id;
505
+ if (!userId)
506
+ throw new Error("OAuth email conflict has no winner");
507
+ await this.assertUserEligible(query, userId);
508
+ } else {
494
509
  throw new Error("OAuth user creation returned no row");
495
- userId = user.id;
510
+ }
496
511
  }
497
- await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
498
- values ($1, $2, $3, $4::jsonb)`, [userId, provider, profile.id, JSON.stringify({ sub: profile.id, email: profile.email, ...profile.metadata })]);
499
- if (!userId)
500
- throw new Error("OAuth identity has no user ID");
501
- return userId;
512
+ const identityData = JSON.stringify({ sub: profile.id, email: profile.email, ...profile.metadata });
513
+ const linked = await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
514
+ values ($1, $2, $3, $4::jsonb)
515
+ on conflict (provider, provider_id) do nothing
516
+ returning user_id`, [userId, provider, profile.id, identityData]);
517
+ if (linked.rows[0])
518
+ return linked.rows[0].user_id;
519
+ const raced = await query(`select user_id from auth.identities where provider = $1 and provider_id = $2`, [provider, profile.id]);
520
+ const racedUserId = raced.rows[0]?.user_id;
521
+ if (!racedUserId)
522
+ throw new Error("OAuth identity conflict has no winner");
523
+ if (createdUserId && createdUserId !== racedUserId) {
524
+ await query(`delete from auth.users where id = $1`, [createdUserId]);
525
+ }
526
+ await this.assertUserEligible(query, racedUserId);
527
+ await query(`update auth.identities set last_sign_in_at = now(), identity_data = $3
528
+ where provider = $1 and provider_id = $2`, [provider, profile.id, identityData]);
529
+ await query(`update auth.users set last_sign_in_at = now() where id = $1`, [racedUserId]);
530
+ return racedUserId;
502
531
  }
503
532
  async assertUserEligible(query, userId) {
504
533
  const users = await query(`select deleted_at is not null as is_deleted,
@@ -1207,6 +1236,12 @@ async function verifyTotp(secretBase32, code, window = 1, atMs = Date.now()) {
1207
1236
  function authError(status, errorCode, msg) {
1208
1237
  return json(status, { code: status, error_code: errorCode, msg });
1209
1238
  }
1239
+ function normalizeEmailInput(value) {
1240
+ if (typeof value !== "string")
1241
+ return null;
1242
+ const email = value.trim().toLowerCase();
1243
+ return email || null;
1244
+ }
1210
1245
  function randomOtp(length) {
1211
1246
  const n = Math.max(6, Math.min(10, Math.floor(length)));
1212
1247
  let code = "";
@@ -1397,8 +1432,10 @@ class AuthHandler {
1397
1432
  } catch (e) {
1398
1433
  if (e instanceof OAuthAccountIneligibleError)
1399
1434
  return authError(400, e.code, e.message);
1400
- const msg = e instanceof Error ? e.message : String(e);
1401
- return authError(500, "unexpected_failure", msg);
1435
+ try {
1436
+ this.config.log?.(`[auth] request_failed code=${databaseDiagnosticCode(e)}`);
1437
+ } catch {}
1438
+ return authError(500, "unexpected_failure", "Unable to complete authentication request");
1402
1439
  }
1403
1440
  }
1404
1441
  async oauthCallbackUrl(req, url) {
@@ -1413,7 +1450,7 @@ class AuthHandler {
1413
1450
  }
1414
1451
  async signup(req) {
1415
1452
  const body = await req.json().catch(() => ({}));
1416
- if (!body.email && !body.password) {
1453
+ if (!("email" in body) && !("password" in body)) {
1417
1454
  if (!this.settings.anonymousUsers) {
1418
1455
  return authError(422, "anonymous_provider_disabled", "Anonymous sign-ins are disabled");
1419
1456
  }
@@ -1425,29 +1462,34 @@ class AuthHandler {
1425
1462
  if (this.settings.disableSignup) {
1426
1463
  return authError(422, "signup_disabled", "Signups not allowed for this instance");
1427
1464
  }
1428
- if (!body.email || !body.password) {
1465
+ const email = normalizeEmailInput(body.email);
1466
+ if (!email || !body.password) {
1429
1467
  return authError(400, "validation_failed", "Signup requires a valid email and password");
1430
1468
  }
1431
1469
  if (body.password.length < this.settings.minPasswordLength) {
1432
1470
  return authError(422, "weak_password", `Password should be at least ${this.settings.minPasswordLength} characters.`);
1433
1471
  }
1434
- const email = body.email.toLowerCase().trim();
1435
- const existing = await this.db.query(`select id from auth.users where email = $1`, [email]);
1436
- if (existing.rows.length > 0) {
1437
- return authError(422, "user_already_exists", "User already registered");
1438
- }
1439
1472
  const hashed = await hashPassword(body.password);
1440
1473
  const autoconfirm = this.settings.autoconfirm;
1441
- const res = await this.db.query(`insert into auth.users
1442
- (aud, role, email, encrypted_password, email_confirmed_at, last_sign_in_at,
1443
- raw_app_meta_data, raw_user_meta_data)
1444
- values ('authenticated', 'authenticated', $1, $2, case when $4 then now() else null end, now(),
1445
- '{"provider":"email","providers":["email"]}', $3)
1446
- returning *`, [email, hashed, JSON.stringify(body.data ?? {}), autoconfirm]);
1447
- const newUser = res.rows[0];
1448
- await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
1449
- values ($1, 'email', $2, $3)
1450
- on conflict (provider, provider_id) do nothing`, [newUser.id, newUser.id, JSON.stringify({ sub: newUser.id, email: newUser.email })]);
1474
+ const newUser = await this.db.transaction(async (query) => {
1475
+ const res = await query(`insert into auth.users
1476
+ (aud, role, email, encrypted_password, email_confirmed_at, last_sign_in_at,
1477
+ raw_app_meta_data, raw_user_meta_data)
1478
+ values ('authenticated', 'authenticated', $1, $2, case when $4 then now() else null end,
1479
+ case when $4 then now() else null end,
1480
+ '{"provider":"email","providers":["email"]}', $3)
1481
+ on conflict (email) do nothing
1482
+ returning *`, [email, hashed, JSON.stringify(body.data ?? {}), autoconfirm]);
1483
+ const user = res.rows[0];
1484
+ if (!user)
1485
+ return null;
1486
+ await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
1487
+ values ($1, 'email', $2, $3)
1488
+ on conflict (provider, provider_id) do nothing`, [user.id, user.id, JSON.stringify({ sub: user.id, email: user.email })]);
1489
+ return user;
1490
+ });
1491
+ if (!newUser)
1492
+ return authError(422, "user_already_exists", "User already registered");
1451
1493
  await this.audit("user_signedup", { actorId: newUser.id, actorEmail: email });
1452
1494
  if (!autoconfirm) {
1453
1495
  await this.issueToken(email, "otp", false, "confirm");
@@ -1461,7 +1503,7 @@ class AuthHandler {
1461
1503
  if (grantType === "password") {
1462
1504
  const email = (body.email ?? "").toLowerCase().trim();
1463
1505
  const res = await this.db.query(`select *, coalesce(banned_until > now(), false) as is_banned
1464
- from auth.users where email = $1 and deleted_at is null`, [email]);
1506
+ from auth.users where lower(email) = $1 and deleted_at is null`, [email]);
1465
1507
  const user = res.rows[0];
1466
1508
  if (user?.encrypted_password && user.is_banned) {
1467
1509
  return authError(400, "user_banned", "User is banned");
@@ -1474,7 +1516,7 @@ class AuthHandler {
1474
1516
  const currentResult = await query(`select *, coalesce(banned_until > now(), false) as is_banned
1475
1517
  from auth.users where id = $1 for update`, [user.id]);
1476
1518
  const current = currentResult.rows[0];
1477
- if (!current || current.deleted_at !== null || !current.encrypted_password || current.email !== email) {
1519
+ if (!current || current.deleted_at !== null || !current.encrypted_password || current.email?.toLowerCase().trim() !== email) {
1478
1520
  return authError(400, "invalid_credentials", "Invalid login credentials");
1479
1521
  }
1480
1522
  if (current.is_banned) {
@@ -1529,18 +1571,12 @@ class AuthHandler {
1529
1571
  const body = await req.json().catch(() => ({}));
1530
1572
  const sets = [];
1531
1573
  const params = [];
1532
- if (body.email && !this.settings.autoconfirm) {
1533
- return authError(501, "email_change_confirmation_not_supported", "Email changes that require confirmation are not supported by SupaCloud Lite V1");
1574
+ const requestedEmail = body.email === undefined ? undefined : normalizeEmailInput(body.email);
1575
+ if (body.email !== undefined && !requestedEmail) {
1576
+ return authError(400, "validation_failed", "email must not be empty");
1534
1577
  }
1535
- const upgradingAnon = (user.is_anonymous ?? false) && !!body.email;
1536
- if (body.email) {
1537
- const email = body.email.toLowerCase().trim();
1538
- const clash = await this.db.query(`select id from auth.users where email = $1 and id <> $2`, [email, user.id]);
1539
- if (clash.rows.length > 0) {
1540
- return authError(422, "email_exists", "A user with this email address has already been registered");
1541
- }
1542
- params.push(email);
1543
- sets.push(`email = $${params.length}, email_confirmed_at = now()`);
1578
+ if (requestedEmail && !this.settings.autoconfirm) {
1579
+ return authError(501, "email_change_confirmation_not_supported", "Email changes that require confirmation are not supported by SupaCloud Lite V1");
1544
1580
  }
1545
1581
  if (body.password) {
1546
1582
  if (body.password.length < this.settings.minPasswordLength) {
@@ -1553,22 +1589,49 @@ class AuthHandler {
1553
1589
  params.push(JSON.stringify(body.data));
1554
1590
  sets.push(`raw_user_meta_data = coalesce(raw_user_meta_data, '{}'::jsonb) || $${params.length}::jsonb`);
1555
1591
  }
1556
- if (upgradingAnon) {
1557
- sets.push(`is_anonymous = false`);
1558
- sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
1559
- }
1560
- if (sets.length === 0) {
1561
- return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
1592
+ let response;
1593
+ try {
1594
+ response = await this.db.transaction(async (query) => {
1595
+ const currentResult = await query(`select * from auth.users where id = $1 and deleted_at is null
1596
+ and (banned_until is null or banned_until <= now()) for update`, [user.id]);
1597
+ const current = currentResult.rows[0];
1598
+ if (!current)
1599
+ return authError(401, "no_authorization", "Invalid or expired token");
1600
+ const transactionSets = [...sets];
1601
+ const transactionParams = [...params];
1602
+ const upgradingAnon = (current.is_anonymous ?? false) && requestedEmail !== undefined;
1603
+ if (requestedEmail) {
1604
+ const email = requestedEmail;
1605
+ const clash = await query(`select id from auth.users where lower(email) = $1 and id <> $2`, [email, current.id]);
1606
+ if (clash.rows.length > 0) {
1607
+ return authError(422, "email_exists", "A user with this email address has already been registered");
1608
+ }
1609
+ transactionParams.push(email);
1610
+ transactionSets.push(`email = $${transactionParams.length}, email_confirmed_at = now()`);
1611
+ }
1612
+ if (upgradingAnon) {
1613
+ transactionSets.push(`is_anonymous = false`);
1614
+ transactionSets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
1615
+ }
1616
+ if (transactionSets.length === 0) {
1617
+ return json(200, await this.userJsonWithRelations(current, query));
1618
+ }
1619
+ transactionParams.push(current.id);
1620
+ const updatedResult = await query(`update auth.users set ${transactionSets.join(", ")}, updated_at = now() where id = $${transactionParams.length} returning *`, transactionParams);
1621
+ const updated = updatedResult.rows[0];
1622
+ if (!updated)
1623
+ return authError(401, "no_authorization", "Invalid or expired token");
1624
+ if (requestedEmail)
1625
+ await this.ensureEmailIdentity(updated, query);
1626
+ return json(200, await this.userJsonWithRelations(updated, query));
1627
+ });
1628
+ } catch (error) {
1629
+ if (databaseDiagnosticCode(error) === "23505" && requestedEmail) {
1630
+ return authError(422, "email_exists", "A user with this email address has already been registered");
1631
+ }
1632
+ throw error;
1562
1633
  }
1563
- params.push(user.id);
1564
- const payload = await this.db.transaction(async (query) => {
1565
- const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1566
- const updated = res.rows[0];
1567
- if (body.email)
1568
- await this.ensureEmailIdentity(updated, query);
1569
- return this.userJsonWithRelations(updated, query);
1570
- });
1571
- return json(200, payload);
1634
+ return response;
1572
1635
  }
1573
1636
  async logout(req, url) {
1574
1637
  const claims = await this.claimsFromBearer(req);
@@ -1594,24 +1657,39 @@ class AuthHandler {
1594
1657
  }
1595
1658
  async issueToken(email, tokenType, createUser, flavor = "login") {
1596
1659
  const normalized = email.toLowerCase().trim();
1597
- let res = await this.db.query(`select * from auth.users where email = $1`, [normalized]);
1598
- let user = res.rows[0];
1599
- if (!user) {
1600
- if (!createUser)
1601
- return json(200, {});
1602
- if (this.settings.disableSignup)
1603
- return authError(422, "signup_disabled", "Signups not allowed for this instance");
1604
- res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
1605
- values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', '{}')
1606
- returning *`, [normalized]);
1607
- user = res.rows[0];
1608
- }
1609
- const code = randomOtp(this.settings.otpLength);
1610
- const linkToken = randomToken(24);
1611
1660
  const expiry = `${this.settings.otpExpirySeconds} seconds`;
1612
- await this.db.query(`delete from auth.one_time_tokens where email = $1 and token_type = $2`, [normalized, tokenType]);
1613
- await this.db.query(`insert into auth.one_time_tokens (user_id, email, token_type, token, expires_at)
1614
- values ($1, $2, $3, $4, now() + $7::interval), ($1, $2, $5, $6, now() + $7::interval)`, [user.id, normalized, tokenType, code, tokenType === "otp" ? "magiclink" : tokenType, linkToken, expiry]);
1661
+ const prepared = await this.db.transaction(async (query) => {
1662
+ let user = (await query(`select * from auth.users where lower(email) = $1 for update`, [normalized])).rows[0];
1663
+ if (!user) {
1664
+ if (!createUser)
1665
+ return { kind: "unknown" };
1666
+ if (this.settings.disableSignup)
1667
+ return { kind: "signup_disabled" };
1668
+ const inserted = await query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
1669
+ values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', '{}')
1670
+ on conflict (email) do nothing
1671
+ returning *`, [normalized]);
1672
+ user = inserted.rows[0];
1673
+ if (!user) {
1674
+ user = (await query(`select * from auth.users where lower(email) = $1 for update`, [normalized])).rows[0];
1675
+ }
1676
+ }
1677
+ if (!user)
1678
+ return { kind: "unknown" };
1679
+ const code = randomOtp(this.settings.otpLength);
1680
+ const linkToken = randomToken(24);
1681
+ const tokenTypes = tokenType === "otp" ? ["otp", "magiclink"] : [tokenType];
1682
+ await query(`delete from auth.one_time_tokens where lower(email) = $1 and token_type = any($2::text[])`, [normalized, tokenTypes]);
1683
+ await query(`insert into auth.one_time_tokens (user_id, email, token_type, token, expires_at)
1684
+ values ($1, $2, $3, $4, now() + $7::interval), ($1, $2, $5, $6, now() + $7::interval)`, [user.id, normalized, tokenType, code, tokenType === "otp" ? "magiclink" : tokenType, linkToken, expiry]);
1685
+ return { kind: "issued", user, code, linkToken };
1686
+ });
1687
+ if (prepared.kind === "unknown")
1688
+ return json(200, {});
1689
+ if (prepared.kind === "signup_disabled") {
1690
+ return authError(422, "signup_disabled", "Signups not allowed for this instance");
1691
+ }
1692
+ const { user, code, linkToken } = prepared;
1615
1693
  const kind = tokenType === "otp" ? "magiclink" : tokenType;
1616
1694
  const link = `${this.config.apiUrl}/auth/v1/verify?token=${linkToken}&type=${kind}`;
1617
1695
  await this.config.mailer.send({
@@ -1640,9 +1718,10 @@ Or sign in with this link: ${link}`
1640
1718
  return authError(400, "validation_failed", "phone must be a valid E.164 number");
1641
1719
  return this.limit("sms", req) ?? this.issuePhoneToken(phone, body.create_user !== false, recordValue(body.data));
1642
1720
  }
1643
- if (!body.email)
1721
+ const email = normalizeEmailInput(body.email);
1722
+ if (!email)
1644
1723
  return authError(400, "validation_failed", "email or phone is required");
1645
- return this.limit("otp", req) ?? this.issueToken(body.email, "otp", body.create_user !== false);
1724
+ return this.limit("otp", req) ?? this.issueToken(email, "otp", body.create_user !== false);
1646
1725
  }
1647
1726
  async issuePhoneToken(phone, createUser, metadata) {
1648
1727
  const sender = this.config.smsSender;
@@ -1796,9 +1875,10 @@ Or sign in with this link: ${link}`
1796
1875
  }
1797
1876
  async sendRecovery(req) {
1798
1877
  const body = await req.json().catch(() => ({}));
1799
- if (!body.email)
1878
+ const email = normalizeEmailInput(body.email);
1879
+ if (!email)
1800
1880
  return authError(400, "validation_failed", "email is required");
1801
- return this.issueToken(body.email, "recovery", false);
1881
+ return this.issueToken(email, "recovery", false);
1802
1882
  }
1803
1883
  static MAX_OTP_ATTEMPTS = 5;
1804
1884
  async redeem(token, types, email) {
@@ -1822,8 +1902,8 @@ Or sign in with this link: ${link}`
1822
1902
  return claimedToken ? this.confirmEmailTokenUser(query, claimedToken) : null;
1823
1903
  }
1824
1904
  async lockAndDeleteEmailToken(query, candidate, redemption) {
1825
- const eligible = await query(`select id from auth.users where id = $1 and deleted_at is null
1826
- and (banned_until is null or banned_until <= now()) for update`, [candidate.user_id]);
1905
+ const eligible = await query(`select id from auth.users where id = $1 and lower(email) = lower($2)
1906
+ and deleted_at is null and (banned_until is null or banned_until <= now()) for update`, [candidate.user_id, candidate.email]);
1827
1907
  if (eligible.rows.length === 0)
1828
1908
  return null;
1829
1909
  const claimedTokens = await query(`delete from auth.one_time_tokens
@@ -1833,15 +1913,19 @@ Or sign in with this link: ${link}`
1833
1913
  return claimedTokens.rows[0] ?? null;
1834
1914
  }
1835
1915
  async confirmEmailTokenUser(query, claimedToken) {
1836
- await query(`delete from auth.one_time_tokens where email = $1`, [claimedToken.email]);
1916
+ await query(`delete from auth.one_time_tokens where lower(email) = lower($1)`, [claimedToken.email]);
1837
1917
  const users = await query(`update auth.users set email_confirmed_at = coalesce(email_confirmed_at, now()), last_sign_in_at = now()
1838
1918
  where id = $1 returning *`, [claimedToken.user_id]);
1839
- return users.rows[0] ?? null;
1919
+ const user = users.rows[0];
1920
+ if (!user)
1921
+ return null;
1922
+ await this.ensureEmailIdentity(user, query);
1923
+ return user;
1840
1924
  }
1841
1925
  async findEmailTokenCandidate(query, redemption) {
1842
1926
  const candidates = await query(`select id, user_id, email from auth.one_time_tokens
1843
1927
  where token = $1 and token_type = any($2::text[])
1844
- and ($3::text is null or email = $3) and expires_at > now()
1928
+ and ($3::text is null or lower(email) = lower($3)) and expires_at > now()
1845
1929
  and attempts < $4`, [redemption.token, redemption.tokenTypes, redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
1846
1930
  return candidates.rows[0] ?? null;
1847
1931
  }
@@ -1849,8 +1933,8 @@ Or sign in with this link: ${link}`
1849
1933
  if (!redemption.email)
1850
1934
  return;
1851
1935
  await query(`update auth.one_time_tokens set attempts = attempts + 1
1852
- where email = $1 and token_type = any($2::text[]) and expires_at > now()`, [redemption.email, redemption.tokenTypes]);
1853
- await query(`delete from auth.one_time_tokens where email = $1 and attempts >= $2`, [redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
1936
+ where lower(email) = lower($1) and token_type = any($2::text[]) and expires_at > now()`, [redemption.email, redemption.tokenTypes]);
1937
+ await query(`delete from auth.one_time_tokens where lower(email) = lower($1) and attempts >= $2`, [redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
1854
1938
  }
1855
1939
  async verifyToken(req) {
1856
1940
  const body = await req.json().catch(() => ({}));
@@ -1886,11 +1970,10 @@ Or sign in with this link: ${link}`
1886
1970
  async redeemPhoneOtp(phone, code) {
1887
1971
  const tokenDigest = `hmac-sha256:v1:${await keyedDigest(this.config.jwtSecret, "supacloud-lite:phone-otp:v1", `${phone}\x00${code}`)}`;
1888
1972
  return this.db.transaction(async (query) => {
1889
- const claimed = await query(`delete from auth.one_time_tokens
1973
+ const candidate = await query(`select user_id from auth.one_time_tokens
1890
1974
  where phone = $1 and token_type = 'sms' and token = $2
1891
- and expires_at > now() and attempts < $3
1892
- returning user_id`, [phone, tokenDigest, AuthHandler.MAX_OTP_ATTEMPTS]);
1893
- const userId = claimed.rows[0]?.user_id;
1975
+ and expires_at > now() and attempts < $3`, [phone, tokenDigest, AuthHandler.MAX_OTP_ATTEMPTS]);
1976
+ const userId = candidate.rows[0]?.user_id;
1894
1977
  if (!userId) {
1895
1978
  const attempt = await query(`update auth.one_time_tokens set attempts = attempts + 1
1896
1979
  where phone = $1 and token_type = 'sms' and expires_at > now()
@@ -1902,6 +1985,22 @@ Or sign in with this link: ${link}`
1902
1985
  await query(`delete from auth.one_time_tokens where phone = $1 and expires_at <= now()`, [phone]);
1903
1986
  return null;
1904
1987
  }
1988
+ const currentUser = await query(`select phone,
1989
+ deleted_at is null and (banned_until is null or banned_until <= now()) as auth_eligible
1990
+ from auth.users where id = $1 for update`, [userId]);
1991
+ const current = currentUser.rows[0];
1992
+ if (!current || current.phone !== phone)
1993
+ return null;
1994
+ if (!current.auth_eligible) {
1995
+ await query(`delete from auth.one_time_tokens where phone = $1`, [phone]);
1996
+ return null;
1997
+ }
1998
+ const claimed = await query(`delete from auth.one_time_tokens
1999
+ where phone = $1 and token_type = 'sms' and token = $2
2000
+ and expires_at > now() and attempts < $3 and user_id = $4
2001
+ returning user_id`, [phone, tokenDigest, AuthHandler.MAX_OTP_ATTEMPTS, userId]);
2002
+ if (claimed.rows.length === 0)
2003
+ return null;
1905
2004
  await query(`delete from auth.one_time_tokens where phone = $1`, [phone]);
1906
2005
  const users = await query(`update auth.users
1907
2006
  set phone_confirmed_at = coalesce(phone_confirmed_at, now()), last_sign_in_at = now(), updated_at = now()
@@ -1966,14 +2065,18 @@ Or sign in with this link: ${link}`
1966
2065
  }
1967
2066
  if (path === "admin/users" && method === "POST") {
1968
2067
  const body = await req.json().catch(() => ({}));
1969
- if (!body.email)
2068
+ const email = normalizeEmailInput(body.email);
2069
+ if (!email)
1970
2070
  return authError(400, "validation_failed", "email is required");
1971
- const email = body.email;
1972
2071
  const hashed = body.password ? await hashPassword(body.password) : null;
1973
2072
  const created = await this.db.transaction(async (query) => {
2073
+ const existing = await query(`select id from auth.users where lower(email) = $1 for update`, [email.toLowerCase().trim()]);
2074
+ if (existing.rows.length > 0)
2075
+ return null;
1974
2076
  const res = await query(`insert into auth.users
1975
2077
  (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1976
2078
  values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
2079
+ on conflict (email) do nothing
1977
2080
  returning *`, [
1978
2081
  email.toLowerCase().trim(),
1979
2082
  hashed,
@@ -1982,9 +2085,13 @@ Or sign in with this link: ${link}`
1982
2085
  JSON.stringify(body.user_metadata ?? {})
1983
2086
  ]);
1984
2087
  const user = res.rows[0];
2088
+ if (!user)
2089
+ return null;
1985
2090
  await this.ensureEmailIdentity(user, query);
1986
2091
  return this.userJsonWithRelations(user, query);
1987
2092
  });
2093
+ if (!created)
2094
+ return authError(422, "email_exists", "A user with this email address has already been registered");
1988
2095
  return json(200, created);
1989
2096
  }
1990
2097
  if (idMatch && method === "GET") {
@@ -1997,8 +2104,11 @@ Or sign in with this link: ${link}`
1997
2104
  const body = await req.json().catch(() => ({}));
1998
2105
  const sets = [];
1999
2106
  const params = [];
2000
- if (typeof body.email === "string") {
2001
- params.push(body.email.toLowerCase().trim());
2107
+ const requestedEmail = body.email === undefined ? undefined : normalizeEmailInput(body.email);
2108
+ if (body.email !== undefined && !requestedEmail)
2109
+ return authError(400, "validation_failed", "email must not be empty");
2110
+ if (requestedEmail) {
2111
+ params.push(requestedEmail);
2002
2112
  sets.push(`email = $${params.length}`);
2003
2113
  }
2004
2114
  if (typeof body.password === "string") {
@@ -2018,15 +2128,33 @@ Or sign in with this link: ${link}`
2018
2128
  if (sets.length === 0)
2019
2129
  return authError(400, "validation_failed", "nothing to update");
2020
2130
  params.push(idMatch[1]);
2021
- const updated = await this.db.transaction(async (query) => {
2022
- const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
2023
- const user = res.rows[0];
2024
- if (!user)
2025
- return null;
2026
- if (typeof body.email === "string")
2027
- await this.ensureEmailIdentity(user, query);
2028
- return this.userJsonWithRelations(user, query);
2029
- });
2131
+ let updated;
2132
+ let emailConflict = false;
2133
+ try {
2134
+ updated = await this.db.transaction(async (query) => {
2135
+ if (requestedEmail) {
2136
+ const clash = await query(`select id from auth.users where lower(email) = $1 and id <> $2`, [requestedEmail, idMatch[1]]);
2137
+ if (clash.rows.length > 0) {
2138
+ emailConflict = true;
2139
+ return null;
2140
+ }
2141
+ }
2142
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
2143
+ const user = res.rows[0];
2144
+ if (!user)
2145
+ return null;
2146
+ if (requestedEmail)
2147
+ await this.ensureEmailIdentity(user, query);
2148
+ return this.userJsonWithRelations(user, query);
2149
+ });
2150
+ } catch (error) {
2151
+ if (databaseDiagnosticCode(error) === "23505" && requestedEmail) {
2152
+ return authError(422, "email_exists", "A user with this email address has already been registered");
2153
+ }
2154
+ throw error;
2155
+ }
2156
+ if (emailConflict)
2157
+ return authError(422, "email_exists", "A user with this email address has already been registered");
2030
2158
  if (!updated)
2031
2159
  return authError(404, "user_not_found", "User not found");
2032
2160
  return json(200, updated);
@@ -2041,26 +2169,30 @@ Or sign in with this link: ${link}`
2041
2169
  if (body.type !== "magiclink") {
2042
2170
  return authError(422, "unsupported_link_type", "Only magiclink generation is supported");
2043
2171
  }
2044
- if (!body.email)
2172
+ const email = normalizeEmailInput(body.email);
2173
+ if (!email)
2045
2174
  return authError(400, "validation_failed", "email is required");
2046
- const email = body.email.toLowerCase().trim();
2047
2175
  const emailOtp = randomOtp(this.settings.otpLength);
2048
2176
  const hashedToken = await sha256Hex(randomToken(32));
2049
2177
  const expiry = `${this.settings.otpExpirySeconds} seconds`;
2050
2178
  const requestUrl = new URL(req.url);
2051
2179
  const redirectTo = resolveRedirect(requestUrl.searchParams.get("redirect_to") ?? body.redirect_to, this.config.siteUrl, this.config.uriAllowList, this.config.enforceRedirectAllowList);
2052
2180
  const user = await this.db.transaction(async (query) => {
2053
- const result = await query(`insert into auth.users
2054
- (aud, role, email, raw_app_meta_data, raw_user_meta_data)
2055
- values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', $2::jsonb)
2056
- on conflict (email) do update set email = excluded.email
2057
- returning *`, [email, JSON.stringify(body.data ?? {})]);
2058
- const linkedUser = result.rows[0];
2181
+ const existing = await query(`select * from auth.users where lower(email) = $1 for update`, [email]);
2182
+ let linkedUser = existing.rows[0];
2183
+ if (!linkedUser) {
2184
+ const result = await query(`insert into auth.users
2185
+ (aud, role, email, raw_app_meta_data, raw_user_meta_data)
2186
+ values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', $2::jsonb)
2187
+ on conflict (email) do update set email = excluded.email
2188
+ returning *`, [email, JSON.stringify(body.data ?? {})]);
2189
+ linkedUser = result.rows[0];
2190
+ }
2059
2191
  await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
2060
2192
  values ($1::uuid, 'email', $1::text, $2::jsonb)
2061
2193
  on conflict (provider, provider_id) do nothing`, [linkedUser.id, JSON.stringify({ sub: linkedUser.id, email })]);
2062
2194
  await query(`delete from auth.one_time_tokens
2063
- where email = $1 and token_type = any($2::text[])`, [email, "{otp,magiclink}"]);
2195
+ where lower(email) = $1 and token_type = any($2::text[])`, [email, "{otp,magiclink}"]);
2064
2196
  await query(`insert into auth.one_time_tokens (user_id, email, token_type, token, expires_at)
2065
2197
  values ($1, $2, 'otp', $3, now() + $5::interval),
2066
2198
  ($1, $2, 'magiclink', $4, now() + $5::interval)`, [linkedUser.id, email, emailOtp, hashedToken, expiry]);
@@ -2152,17 +2284,7 @@ Or sign in with this link: ${link}`
2152
2284
  if (!this.settings.totpEnrollEnabled) {
2153
2285
  return authError(422, "mfa_totp_enroll_disabled", "TOTP enrollment is disabled");
2154
2286
  }
2155
- const enrolled = await this.db.query(`select count(*)::int as n from auth.mfa_factors where user_id = $1`, [user.id]);
2156
- if (enrolled.rows[0].n >= this.settings.maxEnrolledFactors) {
2157
- return authError(422, "too_many_enrolled_mfa_factors", "Maximum number of enrolled MFA factors reached");
2158
- }
2159
2287
  const friendlyName = body.friendly_name ?? null;
2160
- if (friendlyName) {
2161
- const dup = await this.db.query(`select 1 from auth.mfa_factors where user_id = $1 and friendly_name = $2`, [user.id, friendlyName]);
2162
- if (dup.rows.length > 0) {
2163
- return authError(422, "mfa_factor_name_conflict", "A factor with this friendly name already exists");
2164
- }
2165
- }
2166
2288
  const secret = generateTotpSecret();
2167
2289
  let issuer = body.issuer;
2168
2290
  if (!issuer) {
@@ -2172,16 +2294,33 @@ Or sign in with this link: ${link}`
2172
2294
  issuer = "supacloud-lite";
2173
2295
  }
2174
2296
  }
2175
- const uri = otpauthUri({ secret, account: user.email || user.id, issuer });
2176
- const ins = await this.db.query(`insert into auth.mfa_factors (user_id, friendly_name, factor_type, status, secret)
2177
- values ($1, $2, 'totp', 'unverified', $3) returning id`, [user.id, friendlyName, secret]);
2178
- const id = ins.rows[0].id;
2179
- return json(200, {
2180
- id,
2181
- type: "totp",
2182
- friendly_name: friendlyName,
2183
- status: "unverified",
2184
- totp: { qr_code: qrSvgDataUri(uri), secret, uri }
2297
+ return this.db.transaction(async (query) => {
2298
+ const currentResult = await query(`select * from auth.users where id = $1 and deleted_at is null
2299
+ and (banned_until is null or banned_until <= now()) for update`, [user.id]);
2300
+ const current = currentResult.rows[0];
2301
+ if (!current)
2302
+ return authError(401, "no_authorization", "Invalid or expired token");
2303
+ const uri = otpauthUri({ secret, account: current.email || current.id, issuer });
2304
+ const enrolled = await query(`select count(*)::int as n from auth.mfa_factors where user_id = $1`, [current.id]);
2305
+ if (enrolled.rows[0].n >= this.settings.maxEnrolledFactors) {
2306
+ return authError(422, "too_many_enrolled_mfa_factors", "Maximum number of enrolled MFA factors reached");
2307
+ }
2308
+ if (friendlyName) {
2309
+ const dup = await query(`select 1 from auth.mfa_factors where user_id = $1 and friendly_name = $2`, [current.id, friendlyName]);
2310
+ if (dup.rows.length > 0) {
2311
+ return authError(422, "mfa_factor_name_conflict", "A factor with this friendly name already exists");
2312
+ }
2313
+ }
2314
+ const ins = await query(`insert into auth.mfa_factors (user_id, friendly_name, factor_type, status, secret)
2315
+ values ($1, $2, 'totp', 'unverified', $3) returning id`, [current.id, friendlyName, secret]);
2316
+ const id = ins.rows[0].id;
2317
+ return json(200, {
2318
+ id,
2319
+ type: "totp",
2320
+ friendly_name: friendlyName,
2321
+ status: "unverified",
2322
+ totp: { qr_code: qrSvgDataUri(uri), secret, uri }
2323
+ });
2185
2324
  });
2186
2325
  }
2187
2326
  async challengeFactor(req, factorId) {
@@ -2191,22 +2330,31 @@ Or sign in with this link: ${link}`
2191
2330
  if (!this.settings.totpVerifyEnabled) {
2192
2331
  return authError(422, "mfa_totp_verify_disabled", "TOTP verification is disabled");
2193
2332
  }
2194
- const fr = await this.db.query(`select id from auth.mfa_factors where id = $1 and user_id = $2`, [factorId, user.id]);
2195
- if (fr.rows.length === 0)
2196
- return authError(404, "mfa_factor_not_found", "MFA factor not found");
2197
2333
  const expiresAt = new Date(Date.now() + 300000).toISOString();
2198
- const ins = await this.db.query(`insert into auth.mfa_challenges (factor_id, expires_at) values ($1, $2) returning id, expires_at`, [factorId, expiresAt]);
2199
- const c = ins.rows[0];
2200
- return json(200, {
2201
- id: c.id,
2202
- type: "totp",
2203
- expires_at: Math.floor(new Date(c.expires_at).getTime() / 1000)
2334
+ return this.db.transaction(async (query) => {
2335
+ const currentResult = await query(`select * from auth.users where id = $1 and deleted_at is null
2336
+ and (banned_until is null or banned_until <= now()) for update`, [user.id]);
2337
+ if (!currentResult.rows[0])
2338
+ return authError(401, "no_authorization", "Invalid or expired token");
2339
+ const fr = await query(`select id from auth.mfa_factors where id = $1 and user_id = $2`, [factorId, user.id]);
2340
+ if (fr.rows.length === 0)
2341
+ return authError(404, "mfa_factor_not_found", "MFA factor not found");
2342
+ const ins = await query(`insert into auth.mfa_challenges (factor_id, expires_at) values ($1, $2) returning id, expires_at`, [factorId, expiresAt]);
2343
+ const c = ins.rows[0];
2344
+ return json(200, {
2345
+ id: c.id,
2346
+ type: "totp",
2347
+ expires_at: Math.floor(new Date(c.expires_at).getTime() / 1000)
2348
+ });
2204
2349
  });
2205
2350
  }
2206
2351
  async verifyFactor(req, factorId) {
2207
2352
  const user = await this.userFromBearer(req);
2208
2353
  if (!user)
2209
2354
  return authError(401, "no_authorization", "This endpoint requires a Bearer token");
2355
+ if (!this.settings.totpVerifyEnabled) {
2356
+ return authError(422, "mfa_totp_verify_disabled", "TOTP verification is disabled");
2357
+ }
2210
2358
  const body = await req.json().catch(() => ({}));
2211
2359
  const fr = await this.db.query(`select id, secret, status from auth.mfa_factors where id = $1 and user_id = $2`, [factorId, user.id]);
2212
2360
  const factor = fr.rows[0];
@@ -2231,12 +2379,17 @@ Or sign in with this link: ${link}`
2231
2379
  const currentUser = currentUsers.rows[0];
2232
2380
  if (!currentUser)
2233
2381
  return null;
2382
+ const currentFactors = await query(`select id, secret, status from auth.mfa_factors
2383
+ where id = $1 and user_id = $2 for update`, [factorId, user.id]);
2384
+ const currentFactor = currentFactors.rows[0];
2385
+ if (!currentFactor || !await verifyTotp(currentFactor.secret, body.code ?? ""))
2386
+ return null;
2234
2387
  const claimed = await query(`update auth.mfa_challenges set verified_at = now()
2235
2388
  where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
2236
2389
  returning id`, [challenge.id, factorId]);
2237
2390
  if (claimed.rows.length === 0)
2238
2391
  return null;
2239
- if (factor.status !== "verified") {
2392
+ if (currentFactor.status !== "verified") {
2240
2393
  await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2241
2394
  }
2242
2395
  return this.sessionFor(currentUser, undefined, {
@@ -2256,10 +2409,16 @@ Or sign in with this link: ${link}`
2256
2409
  const user = await this.userFromBearer(req);
2257
2410
  if (!user)
2258
2411
  return authError(401, "no_authorization", "This endpoint requires a Bearer token");
2259
- const del = await this.db.query(`delete from auth.mfa_factors where id = $1 and user_id = $2 returning id`, [factorId, user.id]);
2260
- if (del.rows.length === 0)
2261
- return authError(404, "mfa_factor_not_found", "MFA factor not found");
2262
- return json(200, { id: factorId });
2412
+ return this.db.transaction(async (query) => {
2413
+ const currentResult = await query(`select * from auth.users where id = $1 and deleted_at is null
2414
+ and (banned_until is null or banned_until <= now()) for update`, [user.id]);
2415
+ if (!currentResult.rows[0])
2416
+ return authError(401, "no_authorization", "Invalid or expired token");
2417
+ const del = await query(`delete from auth.mfa_factors where id = $1 and user_id = $2 returning id`, [factorId, user.id]);
2418
+ if (del.rows.length === 0)
2419
+ return authError(404, "mfa_factor_not_found", "MFA factor not found");
2420
+ return json(200, { id: factorId });
2421
+ });
2263
2422
  }
2264
2423
  async getUserFactors(userId, query = (sql, params) => this.db.query(sql, params)) {
2265
2424
  const res = await query(`select id, friendly_name, factor_type, status, created_at, updated_at
@@ -9232,7 +9391,8 @@ function applyResize(image, metadata, options) {
9232
9391
  async function transformImage(source, options, knownSourceSize, configuredLimits = {}) {
9233
9392
  const limits = resolveImageTransformLimits(configuredLimits);
9234
9393
  const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
9235
- const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
9394
+ const declaredSourceSize = typeof knownSourceSize === "number" && Number.isFinite(knownSourceSize) && knownSourceSize >= 0 ? knownSourceSize : 0;
9395
+ const sourceSize = Math.max(declaredSourceSize, actualSourceSize);
9236
9396
  if (sourceSize > limits.maxInputBytes) {
9237
9397
  return imageTransformInputTooLarge(limits.maxInputBytes);
9238
9398
  }
@@ -9458,7 +9618,7 @@ function isRenderableActiveType(contentType) {
9458
9618
  return t === "text/html" || t === "application/xhtml+xml" || t === "image/svg+xml" || t === "application/xml" || t === "text/xml";
9459
9619
  }
9460
9620
  var TUS_VERSION = "1.0.0";
9461
- var DEFAULT_FILE_SIZE_LIMIT = 50 * 1024 * 1024;
9621
+ var DEFAULT_FILE_SIZE_LIMIT = 500 * 1024 * 1024;
9462
9622
  var MULTIPART_OVERHEAD_LIMIT = 1024 * 1024;
9463
9623
  var MAX_CONCURRENT_TUS_UPLOADS = 4;
9464
9624
  var MAX_COMPLETED_TUS_UPLOADS = 64;