@supacloud/lite 0.6.0 → 0.7.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 +7 -0
- package/dist/cli.js +100 -24
- package/dist/index.js +100 -24
- package/dist/runtime/auth/handler.d.ts +6 -0
- package/dist/runtime/auth/handler.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.6.0...supacloud-lite-v0.7.0) (2026-08-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **lite:** support admin magic link verification ([#788](https://github.com/zuohuadong/supacloud/issues/788)) ([97defa0](https://github.com/zuohuadong/supacloud/commit/97defa0e3ba875d1bd0363f9f64df02a089238ff))
|
|
9
|
+
|
|
3
10
|
## [0.6.0](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.5.10...supacloud-lite-v0.6.0) (2026-08-11)
|
|
4
11
|
|
|
5
12
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname as dirname5, join as join9, resolve as resolve4 } from "path";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@supacloud/lite",
|
|
11
|
-
version: "0.
|
|
11
|
+
version: "0.7.0",
|
|
12
12
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
|
|
13
13
|
type: "module",
|
|
14
14
|
license: "Apache-2.0",
|
|
@@ -1112,6 +1112,10 @@ function randomOtp(length) {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
return code;
|
|
1114
1114
|
}
|
|
1115
|
+
async function sha256Hex(value) {
|
|
1116
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
1117
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1118
|
+
}
|
|
1115
1119
|
function normalizePhone(value) {
|
|
1116
1120
|
if (typeof value !== "string")
|
|
1117
1121
|
return null;
|
|
@@ -1650,33 +1654,58 @@ Or sign in with this link: ${link}`
|
|
|
1650
1654
|
}
|
|
1651
1655
|
static MAX_OTP_ATTEMPTS = 5;
|
|
1652
1656
|
async redeem(token, types, email) {
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
await this.db.query(`delete from auth.one_time_tokens where email = $1 and attempts >= $2`, [normalizedEmail, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1665
|
-
}
|
|
1657
|
+
const redemption = {
|
|
1658
|
+
token,
|
|
1659
|
+
tokenTypes: `{${types.join(",")}}`,
|
|
1660
|
+
email: email?.toLowerCase().trim() ?? null
|
|
1661
|
+
};
|
|
1662
|
+
return this.db.transaction((query) => this.claimEmailToken(query, redemption));
|
|
1663
|
+
}
|
|
1664
|
+
async claimEmailToken(query, redemption) {
|
|
1665
|
+
const candidate = await this.findEmailTokenCandidate(query, redemption);
|
|
1666
|
+
if (!candidate) {
|
|
1667
|
+
await this.recordFailedEmailTokenAttempt(query, redemption);
|
|
1666
1668
|
return null;
|
|
1667
1669
|
}
|
|
1668
|
-
await this.
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1670
|
+
const claimedToken = await this.lockAndDeleteEmailToken(query, candidate, redemption);
|
|
1671
|
+
return claimedToken ? this.confirmEmailTokenUser(query, claimedToken) : null;
|
|
1672
|
+
}
|
|
1673
|
+
async lockAndDeleteEmailToken(query, candidate, redemption) {
|
|
1674
|
+
await query(`select id from auth.users where id = $1 for update`, [candidate.user_id]);
|
|
1675
|
+
const claimedTokens = await query(`delete from auth.one_time_tokens
|
|
1676
|
+
where id = $1 and token = $2 and token_type = any($3::text[])
|
|
1677
|
+
and expires_at > now() and attempts < $4
|
|
1678
|
+
returning id, user_id, email`, [candidate.id, redemption.token, redemption.tokenTypes, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1679
|
+
return claimedTokens.rows[0] ?? null;
|
|
1680
|
+
}
|
|
1681
|
+
async confirmEmailTokenUser(query, claimedToken) {
|
|
1682
|
+
await query(`delete from auth.one_time_tokens where email = $1`, [claimedToken.email]);
|
|
1683
|
+
const users = await query(`update auth.users set email_confirmed_at = coalesce(email_confirmed_at, now()), last_sign_in_at = now()
|
|
1684
|
+
where id = $1 returning *`, [claimedToken.user_id]);
|
|
1685
|
+
return users.rows[0] ?? null;
|
|
1686
|
+
}
|
|
1687
|
+
async findEmailTokenCandidate(query, redemption) {
|
|
1688
|
+
const candidates = await query(`select id, user_id, email from auth.one_time_tokens
|
|
1689
|
+
where token = $1 and token_type = any($2::text[])
|
|
1690
|
+
and ($3::text is null or email = $3) and expires_at > now()
|
|
1691
|
+
and attempts < $4`, [redemption.token, redemption.tokenTypes, redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1692
|
+
return candidates.rows[0] ?? null;
|
|
1693
|
+
}
|
|
1694
|
+
async recordFailedEmailTokenAttempt(query, redemption) {
|
|
1695
|
+
if (!redemption.email)
|
|
1696
|
+
return;
|
|
1697
|
+
await query(`update auth.one_time_tokens set attempts = attempts + 1
|
|
1698
|
+
where email = $1 and token_type = any($2::text[]) and expires_at > now()`, [redemption.email, redemption.tokenTypes]);
|
|
1699
|
+
await query(`delete from auth.one_time_tokens where email = $1 and attempts >= $2`, [redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1672
1700
|
}
|
|
1673
1701
|
async verifyToken(req) {
|
|
1674
1702
|
const body = await req.json().catch(() => ({}));
|
|
1675
|
-
|
|
1676
|
-
|
|
1703
|
+
const token = body.token ?? body.token_hash;
|
|
1704
|
+
if (!token)
|
|
1705
|
+
return authError(400, "validation_failed", "token or token_hash is required");
|
|
1677
1706
|
if (body.type === "sms" || body.phone !== undefined) {
|
|
1678
1707
|
const phone = normalizePhone(body.phone);
|
|
1679
|
-
if (body.type !== "sms" || !phone || !/^\d{6,10}$/.test(body.token)) {
|
|
1708
|
+
if (body.type !== "sms" || !phone || !body.token || !/^\d{6,10}$/.test(body.token)) {
|
|
1680
1709
|
return authError(400, "validation_failed", "phone, token, and type=sms are required");
|
|
1681
1710
|
}
|
|
1682
1711
|
const fingerprint = await keyedDigest(this.config.jwtSecret, "supacloud-lite:phone-fingerprint:v1", phone);
|
|
@@ -1695,7 +1724,7 @@ Or sign in with this link: ${link}`
|
|
|
1695
1724
|
return json(200, session);
|
|
1696
1725
|
}
|
|
1697
1726
|
const types = body.type === "recovery" ? ["recovery"] : body.type === "magiclink" ? ["magiclink"] : ["otp", "magiclink"];
|
|
1698
|
-
const user = await this.redeem(
|
|
1727
|
+
const user = await this.redeem(token, types, body.email);
|
|
1699
1728
|
if (!user)
|
|
1700
1729
|
return authError(403, "otp_expired", "Token has expired or is invalid");
|
|
1701
1730
|
return json(200, await this.sessionFor(user));
|
|
@@ -1753,6 +1782,9 @@ Or sign in with this link: ${link}`
|
|
|
1753
1782
|
}
|
|
1754
1783
|
const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
|
|
1755
1784
|
const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
|
|
1785
|
+
if (path === "admin/generate_link" && method === "POST") {
|
|
1786
|
+
return await this.generateAdminMagicLink(req);
|
|
1787
|
+
}
|
|
1756
1788
|
if (path === "admin/audit" && method === "GET") {
|
|
1757
1789
|
const res = await this.db.query(`select id, payload, created_at, ip_address from auth.audit_log_entries
|
|
1758
1790
|
order by created_at desc limit 200`);
|
|
@@ -1823,6 +1855,50 @@ Or sign in with this link: ${link}`
|
|
|
1823
1855
|
}
|
|
1824
1856
|
return authError(404, "not_found", `unknown admin endpoint`);
|
|
1825
1857
|
}
|
|
1858
|
+
async generateAdminMagicLink(req) {
|
|
1859
|
+
const body = await req.json().catch(() => ({}));
|
|
1860
|
+
if (body.type !== "magiclink") {
|
|
1861
|
+
return authError(422, "unsupported_link_type", "Only magiclink generation is supported");
|
|
1862
|
+
}
|
|
1863
|
+
if (!body.email)
|
|
1864
|
+
return authError(400, "validation_failed", "email is required");
|
|
1865
|
+
const email = body.email.toLowerCase().trim();
|
|
1866
|
+
const emailOtp = randomOtp(this.settings.otpLength);
|
|
1867
|
+
const hashedToken = await sha256Hex(randomToken(32));
|
|
1868
|
+
const expiry = `${this.settings.otpExpirySeconds} seconds`;
|
|
1869
|
+
const requestUrl = new URL(req.url);
|
|
1870
|
+
const redirectTo = resolveRedirect(requestUrl.searchParams.get("redirect_to") ?? body.redirect_to, this.config.siteUrl, this.config.uriAllowList, this.config.enforceRedirectAllowList);
|
|
1871
|
+
const user = await this.db.transaction(async (query) => {
|
|
1872
|
+
const result = await query(`insert into auth.users
|
|
1873
|
+
(aud, role, email, raw_app_meta_data, raw_user_meta_data)
|
|
1874
|
+
values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', $2::jsonb)
|
|
1875
|
+
on conflict (email) do update set email = excluded.email
|
|
1876
|
+
returning *`, [email, JSON.stringify(body.data ?? {})]);
|
|
1877
|
+
const linkedUser = result.rows[0];
|
|
1878
|
+
await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
1879
|
+
values ($1::uuid, 'email', $1::text, $2::jsonb)
|
|
1880
|
+
on conflict (provider, provider_id) do nothing`, [linkedUser.id, JSON.stringify({ sub: linkedUser.id, email })]);
|
|
1881
|
+
await query(`delete from auth.one_time_tokens
|
|
1882
|
+
where email = $1 and token_type = any($2::text[])`, [email, "{otp,magiclink}"]);
|
|
1883
|
+
await query(`insert into auth.one_time_tokens (user_id, email, token_type, token, expires_at)
|
|
1884
|
+
values ($1, $2, 'otp', $3, now() + $5::interval),
|
|
1885
|
+
($1, $2, 'magiclink', $4, now() + $5::interval)`, [linkedUser.id, email, emailOtp, hashedToken, expiry]);
|
|
1886
|
+
return linkedUser;
|
|
1887
|
+
});
|
|
1888
|
+
const actionUrl = new URL(`${this.config.apiUrl}/auth/v1/verify`);
|
|
1889
|
+
actionUrl.searchParams.set("token", hashedToken);
|
|
1890
|
+
actionUrl.searchParams.set("type", "magiclink");
|
|
1891
|
+
actionUrl.searchParams.set("redirect_to", redirectTo);
|
|
1892
|
+
await this.audit("user_magiclink_requested", { actorId: user.id, actorEmail: email, type: "admin" });
|
|
1893
|
+
return json(200, {
|
|
1894
|
+
...this.userJson(user, [], await this.getUserIdentities(user.id)),
|
|
1895
|
+
action_link: actionUrl.toString(),
|
|
1896
|
+
email_otp: emailOtp,
|
|
1897
|
+
hashed_token: hashedToken,
|
|
1898
|
+
redirect_to: redirectTo,
|
|
1899
|
+
verification_type: "magiclink"
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1826
1902
|
async audit(action, opts = {}) {
|
|
1827
1903
|
try {
|
|
1828
1904
|
const payload = {
|
|
@@ -4148,7 +4224,7 @@ class Database {
|
|
|
4148
4224
|
}
|
|
4149
4225
|
}
|
|
4150
4226
|
if (seedSql) {
|
|
4151
|
-
const hash = await
|
|
4227
|
+
const hash = await sha256Hex2(seedSql);
|
|
4152
4228
|
const seen = await this.engine.query(`select 1 from supabase_migrations.seed_files where path = 'supabase/seed.sql' and hash = $1`, [hash]);
|
|
4153
4229
|
if (seen.rows.length === 0) {
|
|
4154
4230
|
await this.engine.transaction(async (tx) => {
|
|
@@ -4441,7 +4517,7 @@ function quoteIdent(name) {
|
|
|
4441
4517
|
function quoteLiteral(value) {
|
|
4442
4518
|
return `'${value.replaceAll("'", "''")}'`;
|
|
4443
4519
|
}
|
|
4444
|
-
async function
|
|
4520
|
+
async function sha256Hex2(text) {
|
|
4445
4521
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
4446
4522
|
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4447
4523
|
}
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ function randomToken(bytes = 32) {
|
|
|
71
71
|
// package.json
|
|
72
72
|
var package_default = {
|
|
73
73
|
name: "@supacloud/lite",
|
|
74
|
-
version: "0.
|
|
74
|
+
version: "0.7.0",
|
|
75
75
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
|
|
76
76
|
type: "module",
|
|
77
77
|
license: "Apache-2.0",
|
|
@@ -1117,6 +1117,10 @@ function randomOtp(length) {
|
|
|
1117
1117
|
}
|
|
1118
1118
|
return code;
|
|
1119
1119
|
}
|
|
1120
|
+
async function sha256Hex(value) {
|
|
1121
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
1122
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1123
|
+
}
|
|
1120
1124
|
function normalizePhone(value) {
|
|
1121
1125
|
if (typeof value !== "string")
|
|
1122
1126
|
return null;
|
|
@@ -1655,33 +1659,58 @@ Or sign in with this link: ${link}`
|
|
|
1655
1659
|
}
|
|
1656
1660
|
static MAX_OTP_ATTEMPTS = 5;
|
|
1657
1661
|
async redeem(token, types, email) {
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
await this.db.query(`delete from auth.one_time_tokens where email = $1 and attempts >= $2`, [normalizedEmail, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1670
|
-
}
|
|
1662
|
+
const redemption = {
|
|
1663
|
+
token,
|
|
1664
|
+
tokenTypes: `{${types.join(",")}}`,
|
|
1665
|
+
email: email?.toLowerCase().trim() ?? null
|
|
1666
|
+
};
|
|
1667
|
+
return this.db.transaction((query) => this.claimEmailToken(query, redemption));
|
|
1668
|
+
}
|
|
1669
|
+
async claimEmailToken(query, redemption) {
|
|
1670
|
+
const candidate = await this.findEmailTokenCandidate(query, redemption);
|
|
1671
|
+
if (!candidate) {
|
|
1672
|
+
await this.recordFailedEmailTokenAttempt(query, redemption);
|
|
1671
1673
|
return null;
|
|
1672
1674
|
}
|
|
1673
|
-
await this.
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1675
|
+
const claimedToken = await this.lockAndDeleteEmailToken(query, candidate, redemption);
|
|
1676
|
+
return claimedToken ? this.confirmEmailTokenUser(query, claimedToken) : null;
|
|
1677
|
+
}
|
|
1678
|
+
async lockAndDeleteEmailToken(query, candidate, redemption) {
|
|
1679
|
+
await query(`select id from auth.users where id = $1 for update`, [candidate.user_id]);
|
|
1680
|
+
const claimedTokens = await query(`delete from auth.one_time_tokens
|
|
1681
|
+
where id = $1 and token = $2 and token_type = any($3::text[])
|
|
1682
|
+
and expires_at > now() and attempts < $4
|
|
1683
|
+
returning id, user_id, email`, [candidate.id, redemption.token, redemption.tokenTypes, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1684
|
+
return claimedTokens.rows[0] ?? null;
|
|
1685
|
+
}
|
|
1686
|
+
async confirmEmailTokenUser(query, claimedToken) {
|
|
1687
|
+
await query(`delete from auth.one_time_tokens where email = $1`, [claimedToken.email]);
|
|
1688
|
+
const users = await query(`update auth.users set email_confirmed_at = coalesce(email_confirmed_at, now()), last_sign_in_at = now()
|
|
1689
|
+
where id = $1 returning *`, [claimedToken.user_id]);
|
|
1690
|
+
return users.rows[0] ?? null;
|
|
1691
|
+
}
|
|
1692
|
+
async findEmailTokenCandidate(query, redemption) {
|
|
1693
|
+
const candidates = await query(`select id, user_id, email from auth.one_time_tokens
|
|
1694
|
+
where token = $1 and token_type = any($2::text[])
|
|
1695
|
+
and ($3::text is null or email = $3) and expires_at > now()
|
|
1696
|
+
and attempts < $4`, [redemption.token, redemption.tokenTypes, redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1697
|
+
return candidates.rows[0] ?? null;
|
|
1698
|
+
}
|
|
1699
|
+
async recordFailedEmailTokenAttempt(query, redemption) {
|
|
1700
|
+
if (!redemption.email)
|
|
1701
|
+
return;
|
|
1702
|
+
await query(`update auth.one_time_tokens set attempts = attempts + 1
|
|
1703
|
+
where email = $1 and token_type = any($2::text[]) and expires_at > now()`, [redemption.email, redemption.tokenTypes]);
|
|
1704
|
+
await query(`delete from auth.one_time_tokens where email = $1 and attempts >= $2`, [redemption.email, AuthHandler.MAX_OTP_ATTEMPTS]);
|
|
1677
1705
|
}
|
|
1678
1706
|
async verifyToken(req) {
|
|
1679
1707
|
const body = await req.json().catch(() => ({}));
|
|
1680
|
-
|
|
1681
|
-
|
|
1708
|
+
const token = body.token ?? body.token_hash;
|
|
1709
|
+
if (!token)
|
|
1710
|
+
return authError(400, "validation_failed", "token or token_hash is required");
|
|
1682
1711
|
if (body.type === "sms" || body.phone !== undefined) {
|
|
1683
1712
|
const phone = normalizePhone(body.phone);
|
|
1684
|
-
if (body.type !== "sms" || !phone || !/^\d{6,10}$/.test(body.token)) {
|
|
1713
|
+
if (body.type !== "sms" || !phone || !body.token || !/^\d{6,10}$/.test(body.token)) {
|
|
1685
1714
|
return authError(400, "validation_failed", "phone, token, and type=sms are required");
|
|
1686
1715
|
}
|
|
1687
1716
|
const fingerprint = await keyedDigest(this.config.jwtSecret, "supacloud-lite:phone-fingerprint:v1", phone);
|
|
@@ -1700,7 +1729,7 @@ Or sign in with this link: ${link}`
|
|
|
1700
1729
|
return json(200, session);
|
|
1701
1730
|
}
|
|
1702
1731
|
const types = body.type === "recovery" ? ["recovery"] : body.type === "magiclink" ? ["magiclink"] : ["otp", "magiclink"];
|
|
1703
|
-
const user = await this.redeem(
|
|
1732
|
+
const user = await this.redeem(token, types, body.email);
|
|
1704
1733
|
if (!user)
|
|
1705
1734
|
return authError(403, "otp_expired", "Token has expired or is invalid");
|
|
1706
1735
|
return json(200, await this.sessionFor(user));
|
|
@@ -1758,6 +1787,9 @@ Or sign in with this link: ${link}`
|
|
|
1758
1787
|
}
|
|
1759
1788
|
const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
|
|
1760
1789
|
const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
|
|
1790
|
+
if (path === "admin/generate_link" && method === "POST") {
|
|
1791
|
+
return await this.generateAdminMagicLink(req);
|
|
1792
|
+
}
|
|
1761
1793
|
if (path === "admin/audit" && method === "GET") {
|
|
1762
1794
|
const res = await this.db.query(`select id, payload, created_at, ip_address from auth.audit_log_entries
|
|
1763
1795
|
order by created_at desc limit 200`);
|
|
@@ -1828,6 +1860,50 @@ Or sign in with this link: ${link}`
|
|
|
1828
1860
|
}
|
|
1829
1861
|
return authError(404, "not_found", `unknown admin endpoint`);
|
|
1830
1862
|
}
|
|
1863
|
+
async generateAdminMagicLink(req) {
|
|
1864
|
+
const body = await req.json().catch(() => ({}));
|
|
1865
|
+
if (body.type !== "magiclink") {
|
|
1866
|
+
return authError(422, "unsupported_link_type", "Only magiclink generation is supported");
|
|
1867
|
+
}
|
|
1868
|
+
if (!body.email)
|
|
1869
|
+
return authError(400, "validation_failed", "email is required");
|
|
1870
|
+
const email = body.email.toLowerCase().trim();
|
|
1871
|
+
const emailOtp = randomOtp(this.settings.otpLength);
|
|
1872
|
+
const hashedToken = await sha256Hex(randomToken(32));
|
|
1873
|
+
const expiry = `${this.settings.otpExpirySeconds} seconds`;
|
|
1874
|
+
const requestUrl = new URL(req.url);
|
|
1875
|
+
const redirectTo = resolveRedirect(requestUrl.searchParams.get("redirect_to") ?? body.redirect_to, this.config.siteUrl, this.config.uriAllowList, this.config.enforceRedirectAllowList);
|
|
1876
|
+
const user = await this.db.transaction(async (query) => {
|
|
1877
|
+
const result = await query(`insert into auth.users
|
|
1878
|
+
(aud, role, email, raw_app_meta_data, raw_user_meta_data)
|
|
1879
|
+
values ('authenticated', 'authenticated', $1, '{"provider":"email","providers":["email"]}', $2::jsonb)
|
|
1880
|
+
on conflict (email) do update set email = excluded.email
|
|
1881
|
+
returning *`, [email, JSON.stringify(body.data ?? {})]);
|
|
1882
|
+
const linkedUser = result.rows[0];
|
|
1883
|
+
await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
1884
|
+
values ($1::uuid, 'email', $1::text, $2::jsonb)
|
|
1885
|
+
on conflict (provider, provider_id) do nothing`, [linkedUser.id, JSON.stringify({ sub: linkedUser.id, email })]);
|
|
1886
|
+
await query(`delete from auth.one_time_tokens
|
|
1887
|
+
where email = $1 and token_type = any($2::text[])`, [email, "{otp,magiclink}"]);
|
|
1888
|
+
await query(`insert into auth.one_time_tokens (user_id, email, token_type, token, expires_at)
|
|
1889
|
+
values ($1, $2, 'otp', $3, now() + $5::interval),
|
|
1890
|
+
($1, $2, 'magiclink', $4, now() + $5::interval)`, [linkedUser.id, email, emailOtp, hashedToken, expiry]);
|
|
1891
|
+
return linkedUser;
|
|
1892
|
+
});
|
|
1893
|
+
const actionUrl = new URL(`${this.config.apiUrl}/auth/v1/verify`);
|
|
1894
|
+
actionUrl.searchParams.set("token", hashedToken);
|
|
1895
|
+
actionUrl.searchParams.set("type", "magiclink");
|
|
1896
|
+
actionUrl.searchParams.set("redirect_to", redirectTo);
|
|
1897
|
+
await this.audit("user_magiclink_requested", { actorId: user.id, actorEmail: email, type: "admin" });
|
|
1898
|
+
return json(200, {
|
|
1899
|
+
...this.userJson(user, [], await this.getUserIdentities(user.id)),
|
|
1900
|
+
action_link: actionUrl.toString(),
|
|
1901
|
+
email_otp: emailOtp,
|
|
1902
|
+
hashed_token: hashedToken,
|
|
1903
|
+
redirect_to: redirectTo,
|
|
1904
|
+
verification_type: "magiclink"
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1831
1907
|
async audit(action, opts = {}) {
|
|
1832
1908
|
try {
|
|
1833
1909
|
const payload = {
|
|
@@ -4153,7 +4229,7 @@ class Database {
|
|
|
4153
4229
|
}
|
|
4154
4230
|
}
|
|
4155
4231
|
if (seedSql) {
|
|
4156
|
-
const hash = await
|
|
4232
|
+
const hash = await sha256Hex2(seedSql);
|
|
4157
4233
|
const seen = await this.engine.query(`select 1 from supabase_migrations.seed_files where path = 'supabase/seed.sql' and hash = $1`, [hash]);
|
|
4158
4234
|
if (seen.rows.length === 0) {
|
|
4159
4235
|
await this.engine.transaction(async (tx) => {
|
|
@@ -4446,7 +4522,7 @@ function quoteIdent(name) {
|
|
|
4446
4522
|
function quoteLiteral(value) {
|
|
4447
4523
|
return `'${value.replaceAll("'", "''")}'`;
|
|
4448
4524
|
}
|
|
4449
|
-
async function
|
|
4525
|
+
async function sha256Hex2(text) {
|
|
4450
4526
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
4451
4527
|
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4452
4528
|
}
|
|
@@ -119,10 +119,16 @@ export declare class AuthHandler {
|
|
|
119
119
|
/** Max wrong guesses for a one-time code before its tokens are invalidated. */
|
|
120
120
|
private static readonly MAX_OTP_ATTEMPTS;
|
|
121
121
|
private redeem;
|
|
122
|
+
private claimEmailToken;
|
|
123
|
+
private lockAndDeleteEmailToken;
|
|
124
|
+
private confirmEmailTokenUser;
|
|
125
|
+
private findEmailTokenCandidate;
|
|
126
|
+
private recordFailedEmailTokenAttempt;
|
|
122
127
|
private verifyToken;
|
|
123
128
|
private redeemPhoneOtp;
|
|
124
129
|
private verifyLink;
|
|
125
130
|
private admin;
|
|
131
|
+
private generateAdminMagicLink;
|
|
126
132
|
/**
|
|
127
133
|
* Append a security event to auth.audit_log_entries (GoTrue-compatible
|
|
128
134
|
* payload). Best-effort: a logging failure never breaks the request.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAW,MAAM,mBAAmB,CAAA;AAE1D,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAc,SAAS,EAAE,MAAM,aAAa,CAAA;AAEhF,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAGnE,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAExE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG7C,wDAAwD;AACxD,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAA;IACjB,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,yGAAyG;IACzG,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,uEAAuE;IACvE,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,+DAA+D;IAC/D,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAC5B,oFAAoF;IACpF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IACpD,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,KAAK,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CACjC;AAED,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,kBAAkB,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACxC,eAAe,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACrC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACjD,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAClD,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAChC,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kBAAkB,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACxC,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,YAAY,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;CACjC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAW,MAAM,mBAAmB,CAAA;AAE1D,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAc,SAAS,EAAE,MAAM,aAAa,CAAA;AAEhF,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAGnE,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAExE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG7C,wDAAwD;AACxD,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAA;IACjB,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,yGAAyG;IACzG,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,uEAAuE;IACvE,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,+DAA+D;IAC/D,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAC5B,oFAAoF;IACpF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IACpD,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,KAAK,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CACjC;AAED,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,kBAAkB,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACxC,eAAe,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACrC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACjD,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAClD,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAChC,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kBAAkB,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACxC,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,YAAY,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;CACjC;AAgID,wEAAwE;AACxE,qBAAa,WAAW;IAYpB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,MAAM;IAZhB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAM;IACrD,OAAO,CAAC,KAAK,CAAc;IAC3B,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,wBAAwB,CAA6B;IAC7D,OAAO,CAAC,QAAQ,CAAQ;IAExB,YACU,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,UAAU,EAa3B;IAED;;;OAGG;IACH,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,QAAQ;IAUhB,4EAA4E;IACtE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;YAEa,oCAAoC;IAgBlD,8FAA8F;IACxF,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuD3E;YAIa,MAAM;YAiEN,KAAK;YAwCL,OAAO;YAMP,UAAU;YAgEV,MAAM;YAwBN,UAAU;YA4CV,OAAO;YAqBP,eAAe;YAqDf,eAAe;YAmCf,SAAS;YAwBT,kBAAkB;IAShC,OAAO,CAAC,mBAAmB;YASb,eAAe;YAgBf,YAAY;YAaZ,qBAAqB;YAgBrB,qBAAqB;IAUnC,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,kBAAkB;YAQZ,YAAY;IAM1B,+EAA+E;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAI;YAE9B,MAAM;YASN,eAAe;YAWf,uBAAuB;YAkBvB,qBAAqB;YAUrB,uBAAuB;YAcvB,6BAA6B;YAa7B,WAAW;YAsCX,cAAc;YAyDd,UAAU;YAuBV,KAAK;YA6FL,sBAAsB;IAuEpC;;;OAGG;YACW,KAAK;IAqBnB;;;;;OAKG;YACW,UAAU;IA2CxB;;;;;;;;;OASG;YACW,SAAS;YAqBT,YAAY;YAyDZ,eAAe;YAqBf,YAAY;YAyCZ,cAAc;YAWd,cAAc;IAmB5B,oFAAoF;YACtE,iBAAiB;YAuBjB,kBAAkB;YAqBlB,iBAAiB;YAUjB,uBAAuB;IASrC,OAAO,CAAC,kBAAkB;YAoBZ,cAAc;YAOd,gBAAgB;IAM9B,wEAAwE;IACxE,QAAQ,CACN,CAAC,EAAE,OAAO,EACV,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAO,EACvC,UAAU,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAO,GACzC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBzB;IAED,+EAA+E;YACjE,gBAAgB;YAUhB,UAAU;CAqDzB"}
|