@supacloud/lite 0.8.0 → 0.8.1

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.1](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.8.0...supacloud-lite-v0.8.1) (2026-08-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **lite:** complete extended auth compatibility ([#912](https://github.com/zuohuadong/supacloud/issues/912)) ([dd657f5](https://github.com/zuohuadong/supacloud/commit/dd657f5b52f2f3d65c0178f76fd78d027c153c5a))
9
+
3
10
  ## [0.8.0](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.7.3...supacloud-lite-v0.8.0) (2026-08-15)
4
11
 
5
12
 
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { dirname as dirname7, join as join10, resolve as resolve4 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.8.0",
11
+ version: "0.8.1",
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",
@@ -1296,7 +1296,8 @@ class AuthHandler {
1296
1296
  return await this.oauth.authorize(url);
1297
1297
  }
1298
1298
  if (path === "callback" && (method === "GET" || method === "POST")) {
1299
- return await this.oauth.callback(url, (userId) => this.sessionTokensFor(userId));
1299
+ const callbackUrl = method === "POST" ? await this.oauthCallbackUrl(req, url) : url;
1300
+ return await this.oauth.callback(callbackUrl, (userId) => this.oauthSessionTokensFor(userId));
1300
1301
  }
1301
1302
  if (path.startsWith("admin/"))
1302
1303
  return await this.admin(req, ctx, path, method);
@@ -1306,6 +1307,16 @@ class AuthHandler {
1306
1307
  return authError(500, "unexpected_failure", msg);
1307
1308
  }
1308
1309
  }
1310
+ async oauthCallbackUrl(req, url) {
1311
+ const callbackUrl = new URL(url);
1312
+ const form = await req.formData();
1313
+ for (const field of ["code", "state"]) {
1314
+ const value = form.get(field);
1315
+ if (typeof value === "string" && value)
1316
+ callbackUrl.searchParams.set(field, value);
1317
+ }
1318
+ return callbackUrl;
1319
+ }
1309
1320
  async signup(req) {
1310
1321
  const body = await req.json().catch(() => ({}));
1311
1322
  if (!body.email && !body.password) {
@@ -1383,7 +1394,7 @@ class AuthHandler {
1383
1394
  if (!userId)
1384
1395
  return authError(403, "flow_state_not_found", "invalid or expired auth code");
1385
1396
  const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
1386
- return json(200, await this.sessionFor(ures.rows[0]));
1397
+ return json(200, await this.sessionForOAuth(ures.rows[0]));
1387
1398
  }
1388
1399
  return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
1389
1400
  }
@@ -1428,17 +1439,18 @@ class AuthHandler {
1428
1439
  sets.push(`is_anonymous = false`);
1429
1440
  sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
1430
1441
  }
1431
- if (sets.length === 0)
1432
- return json(200, this.userJson(user));
1433
- params.push(user.id);
1434
- const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1435
- const updated = res.rows[0];
1436
- if (upgradingAnon) {
1437
- await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
1438
- values ($1, 'email', $2, $3)
1439
- on conflict (provider, provider_id) do nothing`, [updated.id, updated.id, JSON.stringify({ sub: updated.id, email: updated.email })]);
1442
+ if (sets.length === 0) {
1443
+ return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
1440
1444
  }
1441
- return json(200, this.userJson(updated));
1445
+ params.push(user.id);
1446
+ const payload = await this.db.transaction(async (query) => {
1447
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1448
+ const updated = res.rows[0];
1449
+ if (body.email)
1450
+ await this.ensureEmailIdentity(updated, query);
1451
+ return this.userJsonWithRelations(updated, query);
1452
+ });
1453
+ return json(200, payload);
1442
1454
  }
1443
1455
  async logout(req, url) {
1444
1456
  const claims = await this.claimsFromBearer(req);
@@ -1468,7 +1480,7 @@ class AuthHandler {
1468
1480
  let user = res.rows[0];
1469
1481
  if (!user) {
1470
1482
  if (!createUser)
1471
- return authError(422, "otp_disabled", "Signups not allowed for otp");
1483
+ return json(200, {});
1472
1484
  if (this.settings.disableSignup)
1473
1485
  return authError(422, "signup_disabled", "Signups not allowed for this instance");
1474
1486
  res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
@@ -1800,6 +1812,8 @@ Or sign in with this link: ${link}`
1800
1812
  }
1801
1813
  const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
1802
1814
  const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
1815
+ const factorsMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors$/);
1816
+ const factorMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors\/([0-9a-f-]{36})$/);
1803
1817
  if (path === "admin/generate_link" && method === "POST") {
1804
1818
  return await this.generateAdminMagicLink(req);
1805
1819
  }
@@ -1811,32 +1825,49 @@ Or sign in with this link: ${link}`
1811
1825
  if (exportMatch && method === "GET") {
1812
1826
  return await this.exportUser(exportMatch[1]);
1813
1827
  }
1828
+ if (factorsMatch && method === "GET") {
1829
+ const user = await this.db.query(`select 1 from auth.users where id = $1`, [factorsMatch[1]]);
1830
+ if (user.rows.length === 0)
1831
+ return authError(404, "user_not_found", "User not found");
1832
+ return json(200, await this.getUserFactors(factorsMatch[1]));
1833
+ }
1834
+ if (factorMatch && method === "DELETE") {
1835
+ const deleted = await this.db.query(`delete from auth.mfa_factors where user_id = $1 and id = $2 returning id`, [factorMatch[1], factorMatch[2]]);
1836
+ if (deleted.rows.length === 0)
1837
+ return authError(404, "mfa_factor_not_found", "MFA factor not found");
1838
+ return json(200, { id: factorMatch[2] });
1839
+ }
1814
1840
  if (path === "admin/users" && method === "GET") {
1815
1841
  const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
1816
- return json(200, { users: res.rows.map((u) => this.userJson(u)), aud: "authenticated" });
1842
+ return json(200, { users: res.rows.map((user) => this.userJson(user)), aud: "authenticated" });
1817
1843
  }
1818
1844
  if (path === "admin/users" && method === "POST") {
1819
1845
  const body = await req.json().catch(() => ({}));
1820
1846
  if (!body.email)
1821
1847
  return authError(400, "validation_failed", "email is required");
1822
1848
  const hashed = body.password ? await hashPassword(body.password) : null;
1823
- const res = await this.db.query(`insert into auth.users
1824
- (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1825
- values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
1826
- returning *`, [
1827
- body.email.toLowerCase().trim(),
1828
- hashed,
1829
- body.email_confirm ?? true,
1830
- JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
1831
- JSON.stringify(body.user_metadata ?? {})
1832
- ]);
1833
- return json(200, this.userJson(res.rows[0]));
1849
+ const created = await this.db.transaction(async (query) => {
1850
+ const res = await query(`insert into auth.users
1851
+ (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1852
+ values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
1853
+ returning *`, [
1854
+ body.email.toLowerCase().trim(),
1855
+ hashed,
1856
+ body.email_confirm ?? true,
1857
+ JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
1858
+ JSON.stringify(body.user_metadata ?? {})
1859
+ ]);
1860
+ const user = res.rows[0];
1861
+ await this.ensureEmailIdentity(user, query);
1862
+ return this.userJsonWithRelations(user, query);
1863
+ });
1864
+ return json(200, created);
1834
1865
  }
1835
1866
  if (idMatch && method === "GET") {
1836
1867
  const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
1837
1868
  if (res.rows.length === 0)
1838
1869
  return authError(404, "user_not_found", "User not found");
1839
- return json(200, this.userJson(res.rows[0]));
1870
+ return json(200, await this.userJsonWithRelations(res.rows[0]));
1840
1871
  }
1841
1872
  if (idMatch && method === "PUT") {
1842
1873
  const body = await req.json().catch(() => ({}));
@@ -1863,10 +1894,18 @@ Or sign in with this link: ${link}`
1863
1894
  if (sets.length === 0)
1864
1895
  return authError(400, "validation_failed", "nothing to update");
1865
1896
  params.push(idMatch[1]);
1866
- const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1867
- if (res.rows.length === 0)
1897
+ const updated = await this.db.transaction(async (query) => {
1898
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1899
+ const user = res.rows[0];
1900
+ if (!user)
1901
+ return null;
1902
+ if (typeof body.email === "string")
1903
+ await this.ensureEmailIdentity(user, query);
1904
+ return this.userJsonWithRelations(user, query);
1905
+ });
1906
+ if (!updated)
1868
1907
  return authError(404, "user_not_found", "User not found");
1869
- return json(200, this.userJson(res.rows[0]));
1908
+ return json(200, updated);
1870
1909
  }
1871
1910
  if (idMatch && method === "DELETE") {
1872
1911
  return await this.eraseUser(idMatch[1]);
@@ -2062,17 +2101,26 @@ Or sign in with this link: ${link}`
2062
2101
  if (!await verifyTotp(factor.secret, body.code ?? "")) {
2063
2102
  return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
2064
2103
  }
2065
- await this.db.query(`update auth.mfa_challenges set verified_at = now() where id = $1`, [challenge.id]);
2066
- if (factor.status !== "verified") {
2067
- await this.db.query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2068
- }
2069
- const session = await this.sessionFor(user, undefined, {
2070
- aal: "aal2",
2071
- amr: [
2072
- { method: "password", timestamp: Math.floor(Date.now() / 1000) },
2073
- { method: "totp", timestamp: Math.floor(Date.now() / 1000) }
2074
- ]
2104
+ const session = await this.db.transaction(async (query) => {
2105
+ const claimed = await query(`update auth.mfa_challenges set verified_at = now()
2106
+ where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
2107
+ returning id`, [challenge.id, factorId]);
2108
+ if (claimed.rows.length === 0)
2109
+ return null;
2110
+ if (factor.status !== "verified") {
2111
+ await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2112
+ }
2113
+ return this.sessionFor(user, undefined, {
2114
+ aal: "aal2",
2115
+ amr: [
2116
+ { method: "password", timestamp: Math.floor(Date.now() / 1000) },
2117
+ { method: "totp", timestamp: Math.floor(Date.now() / 1000) }
2118
+ ]
2119
+ }, query);
2075
2120
  });
2121
+ if (!session) {
2122
+ return authError(422, "mfa_verification_failed", "This challenge has already been verified");
2123
+ }
2076
2124
  return json(200, session);
2077
2125
  }
2078
2126
  async unenrollFactor(req, factorId) {
@@ -2110,6 +2158,17 @@ Or sign in with this link: ${link}`
2110
2158
  updated_at: iso(r.updated_at)
2111
2159
  }));
2112
2160
  }
2161
+ async ensureEmailIdentity(user, query = (sql, params) => this.db.query(sql, params)) {
2162
+ if (!user.email)
2163
+ return;
2164
+ await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
2165
+ values ($1::uuid, 'email', $1::text, $2::jsonb)
2166
+ on conflict (provider, provider_id) do update
2167
+ set identity_data = excluded.identity_data, updated_at = now()`, [user.id, JSON.stringify({ sub: user.id, email: user.email })]);
2168
+ }
2169
+ async userJsonWithRelations(user, query = (sql, params) => this.db.query(sql, params)) {
2170
+ return this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query));
2171
+ }
2113
2172
  async rotateRefreshToken(token) {
2114
2173
  return this.db.transaction(async (query) => {
2115
2174
  const claimedToken = await this.claimRefreshToken(query, token);
@@ -2183,9 +2242,14 @@ Or sign in with this link: ${link}`
2183
2242
  is_anonymous: u.is_anonymous ?? false
2184
2243
  };
2185
2244
  }
2186
- async sessionTokensFor(userId) {
2245
+ sessionForOAuth(user) {
2246
+ return this.sessionFor(user, undefined, {
2247
+ amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
2248
+ });
2249
+ }
2250
+ async oauthSessionTokensFor(userId) {
2187
2251
  const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
2188
- const session = await this.sessionFor(res.rows[0]);
2252
+ const session = await this.sessionForOAuth(res.rows[0]);
2189
2253
  return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
2190
2254
  }
2191
2255
  async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
package/dist/index.js CHANGED
@@ -85,7 +85,7 @@ function randomToken(bytes = 32) {
85
85
  // package.json
86
86
  var package_default = {
87
87
  name: "@supacloud/lite",
88
- version: "0.8.0",
88
+ version: "0.8.1",
89
89
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
90
90
  type: "module",
91
91
  license: "Apache-2.0",
@@ -1301,7 +1301,8 @@ class AuthHandler {
1301
1301
  return await this.oauth.authorize(url);
1302
1302
  }
1303
1303
  if (path === "callback" && (method === "GET" || method === "POST")) {
1304
- return await this.oauth.callback(url, (userId) => this.sessionTokensFor(userId));
1304
+ const callbackUrl = method === "POST" ? await this.oauthCallbackUrl(req, url) : url;
1305
+ return await this.oauth.callback(callbackUrl, (userId) => this.oauthSessionTokensFor(userId));
1305
1306
  }
1306
1307
  if (path.startsWith("admin/"))
1307
1308
  return await this.admin(req, ctx, path, method);
@@ -1311,6 +1312,16 @@ class AuthHandler {
1311
1312
  return authError(500, "unexpected_failure", msg);
1312
1313
  }
1313
1314
  }
1315
+ async oauthCallbackUrl(req, url) {
1316
+ const callbackUrl = new URL(url);
1317
+ const form = await req.formData();
1318
+ for (const field of ["code", "state"]) {
1319
+ const value = form.get(field);
1320
+ if (typeof value === "string" && value)
1321
+ callbackUrl.searchParams.set(field, value);
1322
+ }
1323
+ return callbackUrl;
1324
+ }
1314
1325
  async signup(req) {
1315
1326
  const body = await req.json().catch(() => ({}));
1316
1327
  if (!body.email && !body.password) {
@@ -1388,7 +1399,7 @@ class AuthHandler {
1388
1399
  if (!userId)
1389
1400
  return authError(403, "flow_state_not_found", "invalid or expired auth code");
1390
1401
  const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
1391
- return json(200, await this.sessionFor(ures.rows[0]));
1402
+ return json(200, await this.sessionForOAuth(ures.rows[0]));
1392
1403
  }
1393
1404
  return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
1394
1405
  }
@@ -1433,17 +1444,18 @@ class AuthHandler {
1433
1444
  sets.push(`is_anonymous = false`);
1434
1445
  sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
1435
1446
  }
1436
- if (sets.length === 0)
1437
- return json(200, this.userJson(user));
1438
- params.push(user.id);
1439
- const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1440
- const updated = res.rows[0];
1441
- if (upgradingAnon) {
1442
- await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
1443
- values ($1, 'email', $2, $3)
1444
- on conflict (provider, provider_id) do nothing`, [updated.id, updated.id, JSON.stringify({ sub: updated.id, email: updated.email })]);
1447
+ if (sets.length === 0) {
1448
+ return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
1445
1449
  }
1446
- return json(200, this.userJson(updated));
1450
+ params.push(user.id);
1451
+ const payload = await this.db.transaction(async (query) => {
1452
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1453
+ const updated = res.rows[0];
1454
+ if (body.email)
1455
+ await this.ensureEmailIdentity(updated, query);
1456
+ return this.userJsonWithRelations(updated, query);
1457
+ });
1458
+ return json(200, payload);
1447
1459
  }
1448
1460
  async logout(req, url) {
1449
1461
  const claims = await this.claimsFromBearer(req);
@@ -1473,7 +1485,7 @@ class AuthHandler {
1473
1485
  let user = res.rows[0];
1474
1486
  if (!user) {
1475
1487
  if (!createUser)
1476
- return authError(422, "otp_disabled", "Signups not allowed for otp");
1488
+ return json(200, {});
1477
1489
  if (this.settings.disableSignup)
1478
1490
  return authError(422, "signup_disabled", "Signups not allowed for this instance");
1479
1491
  res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
@@ -1805,6 +1817,8 @@ Or sign in with this link: ${link}`
1805
1817
  }
1806
1818
  const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
1807
1819
  const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
1820
+ const factorsMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors$/);
1821
+ const factorMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors\/([0-9a-f-]{36})$/);
1808
1822
  if (path === "admin/generate_link" && method === "POST") {
1809
1823
  return await this.generateAdminMagicLink(req);
1810
1824
  }
@@ -1816,32 +1830,49 @@ Or sign in with this link: ${link}`
1816
1830
  if (exportMatch && method === "GET") {
1817
1831
  return await this.exportUser(exportMatch[1]);
1818
1832
  }
1833
+ if (factorsMatch && method === "GET") {
1834
+ const user = await this.db.query(`select 1 from auth.users where id = $1`, [factorsMatch[1]]);
1835
+ if (user.rows.length === 0)
1836
+ return authError(404, "user_not_found", "User not found");
1837
+ return json(200, await this.getUserFactors(factorsMatch[1]));
1838
+ }
1839
+ if (factorMatch && method === "DELETE") {
1840
+ const deleted = await this.db.query(`delete from auth.mfa_factors where user_id = $1 and id = $2 returning id`, [factorMatch[1], factorMatch[2]]);
1841
+ if (deleted.rows.length === 0)
1842
+ return authError(404, "mfa_factor_not_found", "MFA factor not found");
1843
+ return json(200, { id: factorMatch[2] });
1844
+ }
1819
1845
  if (path === "admin/users" && method === "GET") {
1820
1846
  const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
1821
- return json(200, { users: res.rows.map((u) => this.userJson(u)), aud: "authenticated" });
1847
+ return json(200, { users: res.rows.map((user) => this.userJson(user)), aud: "authenticated" });
1822
1848
  }
1823
1849
  if (path === "admin/users" && method === "POST") {
1824
1850
  const body = await req.json().catch(() => ({}));
1825
1851
  if (!body.email)
1826
1852
  return authError(400, "validation_failed", "email is required");
1827
1853
  const hashed = body.password ? await hashPassword(body.password) : null;
1828
- const res = await this.db.query(`insert into auth.users
1829
- (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1830
- values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
1831
- returning *`, [
1832
- body.email.toLowerCase().trim(),
1833
- hashed,
1834
- body.email_confirm ?? true,
1835
- JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
1836
- JSON.stringify(body.user_metadata ?? {})
1837
- ]);
1838
- return json(200, this.userJson(res.rows[0]));
1854
+ const created = await this.db.transaction(async (query) => {
1855
+ const res = await query(`insert into auth.users
1856
+ (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1857
+ values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
1858
+ returning *`, [
1859
+ body.email.toLowerCase().trim(),
1860
+ hashed,
1861
+ body.email_confirm ?? true,
1862
+ JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
1863
+ JSON.stringify(body.user_metadata ?? {})
1864
+ ]);
1865
+ const user = res.rows[0];
1866
+ await this.ensureEmailIdentity(user, query);
1867
+ return this.userJsonWithRelations(user, query);
1868
+ });
1869
+ return json(200, created);
1839
1870
  }
1840
1871
  if (idMatch && method === "GET") {
1841
1872
  const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
1842
1873
  if (res.rows.length === 0)
1843
1874
  return authError(404, "user_not_found", "User not found");
1844
- return json(200, this.userJson(res.rows[0]));
1875
+ return json(200, await this.userJsonWithRelations(res.rows[0]));
1845
1876
  }
1846
1877
  if (idMatch && method === "PUT") {
1847
1878
  const body = await req.json().catch(() => ({}));
@@ -1868,10 +1899,18 @@ Or sign in with this link: ${link}`
1868
1899
  if (sets.length === 0)
1869
1900
  return authError(400, "validation_failed", "nothing to update");
1870
1901
  params.push(idMatch[1]);
1871
- const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1872
- if (res.rows.length === 0)
1902
+ const updated = await this.db.transaction(async (query) => {
1903
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1904
+ const user = res.rows[0];
1905
+ if (!user)
1906
+ return null;
1907
+ if (typeof body.email === "string")
1908
+ await this.ensureEmailIdentity(user, query);
1909
+ return this.userJsonWithRelations(user, query);
1910
+ });
1911
+ if (!updated)
1873
1912
  return authError(404, "user_not_found", "User not found");
1874
- return json(200, this.userJson(res.rows[0]));
1913
+ return json(200, updated);
1875
1914
  }
1876
1915
  if (idMatch && method === "DELETE") {
1877
1916
  return await this.eraseUser(idMatch[1]);
@@ -2067,17 +2106,26 @@ Or sign in with this link: ${link}`
2067
2106
  if (!await verifyTotp(factor.secret, body.code ?? "")) {
2068
2107
  return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
2069
2108
  }
2070
- await this.db.query(`update auth.mfa_challenges set verified_at = now() where id = $1`, [challenge.id]);
2071
- if (factor.status !== "verified") {
2072
- await this.db.query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2073
- }
2074
- const session = await this.sessionFor(user, undefined, {
2075
- aal: "aal2",
2076
- amr: [
2077
- { method: "password", timestamp: Math.floor(Date.now() / 1000) },
2078
- { method: "totp", timestamp: Math.floor(Date.now() / 1000) }
2079
- ]
2109
+ const session = await this.db.transaction(async (query) => {
2110
+ const claimed = await query(`update auth.mfa_challenges set verified_at = now()
2111
+ where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
2112
+ returning id`, [challenge.id, factorId]);
2113
+ if (claimed.rows.length === 0)
2114
+ return null;
2115
+ if (factor.status !== "verified") {
2116
+ await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2117
+ }
2118
+ return this.sessionFor(user, undefined, {
2119
+ aal: "aal2",
2120
+ amr: [
2121
+ { method: "password", timestamp: Math.floor(Date.now() / 1000) },
2122
+ { method: "totp", timestamp: Math.floor(Date.now() / 1000) }
2123
+ ]
2124
+ }, query);
2080
2125
  });
2126
+ if (!session) {
2127
+ return authError(422, "mfa_verification_failed", "This challenge has already been verified");
2128
+ }
2081
2129
  return json(200, session);
2082
2130
  }
2083
2131
  async unenrollFactor(req, factorId) {
@@ -2115,6 +2163,17 @@ Or sign in with this link: ${link}`
2115
2163
  updated_at: iso(r.updated_at)
2116
2164
  }));
2117
2165
  }
2166
+ async ensureEmailIdentity(user, query = (sql, params) => this.db.query(sql, params)) {
2167
+ if (!user.email)
2168
+ return;
2169
+ await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
2170
+ values ($1::uuid, 'email', $1::text, $2::jsonb)
2171
+ on conflict (provider, provider_id) do update
2172
+ set identity_data = excluded.identity_data, updated_at = now()`, [user.id, JSON.stringify({ sub: user.id, email: user.email })]);
2173
+ }
2174
+ async userJsonWithRelations(user, query = (sql, params) => this.db.query(sql, params)) {
2175
+ return this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query));
2176
+ }
2118
2177
  async rotateRefreshToken(token) {
2119
2178
  return this.db.transaction(async (query) => {
2120
2179
  const claimedToken = await this.claimRefreshToken(query, token);
@@ -2188,9 +2247,14 @@ Or sign in with this link: ${link}`
2188
2247
  is_anonymous: u.is_anonymous ?? false
2189
2248
  };
2190
2249
  }
2191
- async sessionTokensFor(userId) {
2250
+ sessionForOAuth(user) {
2251
+ return this.sessionFor(user, undefined, {
2252
+ amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
2253
+ });
2254
+ }
2255
+ async oauthSessionTokensFor(userId) {
2192
2256
  const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
2193
- const session = await this.sessionFor(res.rows[0]);
2257
+ const session = await this.sessionForOAuth(res.rows[0]);
2194
2258
  return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
2195
2259
  }
2196
2260
  async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
@@ -97,6 +97,7 @@ export declare class AuthHandler {
97
97
  private phoneDeliveriesSettledBeforeDeadline;
98
98
  /** Dispatch one `/auth/v1/*` request. Any thrown error becomes a 500 `unexpected_failure`. */
99
99
  handle(req: Request, ctx: RequestContext, url: URL): Promise<Response>;
100
+ private oauthCallbackUrl;
100
101
  private signup;
101
102
  private token;
102
103
  private getUser;
@@ -159,6 +160,8 @@ export declare class AuthHandler {
159
160
  private getUserFactors;
160
161
  /** GoTrue-shaped identities for a user (linked providers), from auth.identities. */
161
162
  private getUserIdentities;
163
+ private ensureEmailIdentity;
164
+ private userJsonWithRelations;
162
165
  private rotateRefreshToken;
163
166
  private claimRefreshToken;
164
167
  private refreshSessionStartedAt;
@@ -167,8 +170,8 @@ export declare class AuthHandler {
167
170
  private claimsFromBearer;
168
171
  /** Shape a user row into the GoTrue user object supabase-js expects. */
169
172
  userJson(u: UserRow, factors?: Record<string, unknown>[], identities?: Record<string, unknown>[]): Record<string, unknown>;
170
- /** Session tokens for a bare user id (used by the OAuth implicit callback). */
171
- private sessionTokensFor;
173
+ private sessionForOAuth;
174
+ private oauthSessionTokensFor;
172
175
  private sessionFor;
173
176
  }
174
177
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/handler.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/lite",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",