@edge-base/server 0.2.8 → 0.3.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/admin-build/_app/immutable/chunks/{zl2AUKMP.js → BSIzoJ5I.js} +1 -1
- package/admin-build/_app/immutable/chunks/{j4jxnAKj.js → BeYswbvA.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DcVb45Ds.js → BkrC_6Ss.js} +1 -1
- package/admin-build/_app/immutable/chunks/{fPy6xmgG.js → BunyH6GE.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B9efkx2V.js → C-zXt-cq.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DYtrHeVQ.js → C1Mycuvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Bt4AyT3o.js → Cek51lkE.js} +3 -3
- package/admin-build/_app/immutable/chunks/{D8aeTKry.js → D0mNYcxN.js} +1 -1
- package/admin-build/_app/immutable/chunks/D3hdqsfp.js +1 -0
- package/admin-build/_app/immutable/chunks/{DPgR4-0v.js → DF40xjpj.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTRjWhGs.js → DOOhHO-v.js} +1 -1
- package/admin-build/_app/immutable/chunks/DdjFeYwG.js +1 -0
- package/admin-build/_app/immutable/chunks/{CwyE59Yt.js → Dvlxm2Iq.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BMXWUTG-.js → Xm1dHMTE.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DGAHkap7.js → aPulNooa.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CMYgGhZR.js → dZsKGnWZ.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.Cmz0WjMl.js → app.j1WHBlZX.js} +2 -2
- package/admin-build/_app/immutable/entry/start.BgMeGq-q.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.y6D_QyUb.js → 0.Oo7ZpbR-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.CndRxhbH.js → 1.zgMnKHns.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.CdA5FmXy.js → 10.u5mkRiUe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.DG8SzMp_.js → 11.9ynAgaxV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.CvmQqpFa.js → 12.M_EqzJ6s.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.BbGNdswT.js → 13.D_NQ9q60.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CZKsN7-O.js → 14.-vENIJ2w.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.A7-CYgkG.js → 15.DoMCZhGv.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.hgJT9H-x.js → 16.6BmnzScq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DkWZbcN2.js → 17.q6QQ9N_J.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.sX3Fb5gh.js → 18.DUhJ6dKj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.VAZUW-1K.js → 19.CvkvPlGP.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.DkIKxacG.js → 20.cACSbRIl.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.DZ7G8rMF.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.BDaHvtaw.js → 22.BsQ6xeKr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.BVRzw_pD.js → 23.CNW3mLz5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CVhSJyG0.js → 24.CReqtEOy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.Bme-9bZn.js → 25.BuxVhqlb.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.Dsx7RIIs.js → 26.C-LzPf62.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DMGQnzFM.js → 27.Dl5RL_tB.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.GGwFmEhZ.js → 28.CWfSQgjH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Dnghr0nk.js → 29.wUSfh2eQ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.Cg7zZJP1.js → 3.D_laBXeK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.C0J24z3I.js → 30.DcMhZyQ0.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.MdxFI8v6.js → 31.CSwhNsNi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.DCAOVzGE.js → 4.Cq28vPI2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.DzUQ-cTc.js → 5.BTT9B3oI.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.CptBYTVj.js → 6.iCmmzXZp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.DfeeQ0Rg.js → 7.qY5v7qqJ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.CIcvctW7.js → 8.C6p4RvgK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.QKrvq4RA.js → 9.f548N7zh.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +305 -3
- package/package.json +3 -3
- package/src/__tests__/auth-token.test.ts +142 -0
- package/src/__tests__/email-provider.test.ts +82 -0
- package/src/__tests__/functions-context.test.ts +87 -0
- package/src/__tests__/functions-d1-proxy.test.ts +70 -0
- package/src/__tests__/functions-route.test.ts +15 -0
- package/src/__tests__/room-access-policy.test.ts +26 -0
- package/src/__tests__/room-handler-context.test.ts +2 -0
- package/src/__tests__/smoke-skip-report.test.ts +1 -1
- package/src/durable-objects/room-runtime-base.ts +94 -11
- package/src/durable-objects/rooms-do.ts +4 -1
- package/src/lib/auth-d1-service.ts +37 -7
- package/src/lib/auth-token.ts +66 -0
- package/src/lib/email-provider.ts +115 -5
- package/src/lib/functions.ts +91 -1
- package/src/lib/internal-transport.ts +15 -1
- package/src/lib/jwt.ts +2 -0
- package/src/routes/admin.ts +8 -1
- package/src/routes/auth.ts +111 -19
- package/src/routes/room.ts +36 -2
- package/src/routes/storage.ts +446 -62
- package/src/types.ts +10 -0
- package/admin-build/_app/immutable/chunks/CKVjMXZi.js +0 -1
- package/admin-build/_app/immutable/chunks/Djnkhy-S.js +0 -1
- package/admin-build/_app/immutable/entry/start.JE7dcbK1.js +0 -1
- package/admin-build/_app/immutable/nodes/21.DOjJlQKc.js +0 -1
package/src/routes/auth.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
buildEmailActionUrl,
|
|
28
28
|
parseClientRedirectInput,
|
|
29
29
|
} from '../lib/auth-redirect.js';
|
|
30
|
+
import { hashOtpSecret, verifyOtpSecret } from '../lib/auth-token.js';
|
|
30
31
|
import {
|
|
31
32
|
signAccessToken, signRefreshToken, verifyRefreshTokenWithFallback,
|
|
32
33
|
parseDuration, TokenExpiredError,
|
|
@@ -176,7 +177,7 @@ function getMaxActiveSessions(env: Env): number {
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
function isEmailProviderName(value: unknown): value is EmailConfig['provider'] {
|
|
179
|
-
return value === 'resend' || value === 'sendgrid' || value === 'mailgun' || value === 'ses';
|
|
180
|
+
return value === 'resend' || value === 'sendgrid' || value === 'mailgun' || value === 'ses' || value === 'cloudflare';
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
function getEmailConfig(env: Env): EmailConfig | undefined {
|
|
@@ -187,22 +188,29 @@ function getEmailConfig(env: Env): EmailConfig | undefined {
|
|
|
187
188
|
const provider = configured?.provider
|
|
188
189
|
?? (isEmailProviderName(runtimeEnv.EDGEBASE_EMAIL_PROVIDER) ? runtimeEnv.EDGEBASE_EMAIL_PROVIDER : undefined);
|
|
189
190
|
const apiKey = configured?.apiKey
|
|
190
|
-
?? (typeof runtimeEnv.EDGEBASE_EMAIL_API_KEY === 'string' ? runtimeEnv.EDGEBASE_EMAIL_API_KEY : undefined)
|
|
191
|
+
?? (typeof runtimeEnv.EDGEBASE_EMAIL_API_KEY === 'string' ? runtimeEnv.EDGEBASE_EMAIL_API_KEY : undefined)
|
|
192
|
+
?? (typeof runtimeEnv.EDGEBASE_EMAIL_CLOUDFLARE_API_TOKEN === 'string' ? runtimeEnv.EDGEBASE_EMAIL_CLOUDFLARE_API_TOKEN : undefined);
|
|
191
193
|
const from = configured?.from
|
|
192
194
|
?? (typeof runtimeEnv.EDGEBASE_EMAIL_FROM === 'string' ? runtimeEnv.EDGEBASE_EMAIL_FROM : undefined);
|
|
195
|
+
const cloudflareAccountId = configured?.accountId
|
|
196
|
+
?? (typeof runtimeEnv.EDGEBASE_EMAIL_CLOUDFLARE_ACCOUNT_ID === 'string' ? runtimeEnv.EDGEBASE_EMAIL_CLOUDFLARE_ACCOUNT_ID : undefined);
|
|
197
|
+
const cloudflareBinding = configured?.binding
|
|
198
|
+
?? (typeof runtimeEnv.EDGEBASE_EMAIL_CLOUDFLARE_BINDING === 'string' ? runtimeEnv.EDGEBASE_EMAIL_CLOUDFLARE_BINDING : undefined);
|
|
193
199
|
|
|
194
|
-
if (!provider || !
|
|
200
|
+
if (!provider || !from || (provider !== 'cloudflare' && !apiKey)) {
|
|
195
201
|
return configured;
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
return {
|
|
199
205
|
provider,
|
|
200
|
-
apiKey,
|
|
201
206
|
from,
|
|
207
|
+
...(apiKey ? { apiKey } : {}),
|
|
202
208
|
domain: configured?.domain
|
|
203
209
|
?? (typeof runtimeEnv.EDGEBASE_EMAIL_MAILGUN_DOMAIN === 'string' ? runtimeEnv.EDGEBASE_EMAIL_MAILGUN_DOMAIN : undefined),
|
|
204
210
|
region: configured?.region
|
|
205
211
|
?? (typeof runtimeEnv.EDGEBASE_EMAIL_SES_REGION === 'string' ? runtimeEnv.EDGEBASE_EMAIL_SES_REGION : undefined),
|
|
212
|
+
accountId: cloudflareAccountId,
|
|
213
|
+
binding: cloudflareBinding,
|
|
206
214
|
appName: configured?.appName ?? 'EdgeBase Local Auth Harness',
|
|
207
215
|
defaultLocale: configured?.defaultLocale,
|
|
208
216
|
verifyUrl: configured?.verifyUrl
|
|
@@ -539,7 +547,7 @@ async function rotateRefreshTokenFlow(
|
|
|
539
547
|
}
|
|
540
548
|
|
|
541
549
|
const newRefreshToken = await signRefreshToken(
|
|
542
|
-
{ sub: userId, type: 'refresh', jti:
|
|
550
|
+
{ sub: userId, type: 'refresh', jti: session.id as string },
|
|
543
551
|
getUserSecret(env),
|
|
544
552
|
getRefreshTokenTTL(env),
|
|
545
553
|
);
|
|
@@ -781,7 +789,11 @@ async function sendMailWithHook(
|
|
|
781
789
|
}
|
|
782
790
|
}
|
|
783
791
|
|
|
784
|
-
|
|
792
|
+
const result = await provider.send({ to, subject: finalSubject, html: finalHtml });
|
|
793
|
+
if (!result.success) {
|
|
794
|
+
throw new EdgeBaseError(502, 'Email delivery failed.', undefined, 'email-delivery-failed');
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
785
797
|
}
|
|
786
798
|
|
|
787
799
|
async function sendSmsWithHook(
|
|
@@ -1452,7 +1464,7 @@ authRoute.openapi(signinMagicLink, async (c) => {
|
|
|
1452
1464
|
await sendMailWithHook(
|
|
1453
1465
|
c.env, c.executionCtx, provider, 'magicLink', body.email,
|
|
1454
1466
|
resolveSubject(c.env, 'magicLink', defaultSubject, locale), html, locale,
|
|
1455
|
-
)
|
|
1467
|
+
);
|
|
1456
1468
|
} else {
|
|
1457
1469
|
if (!isReleaseMode(c.env)) {
|
|
1458
1470
|
debugToken = token;
|
|
@@ -1627,10 +1639,12 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1627
1639
|
const code = generateOTP();
|
|
1628
1640
|
if (exposeTestSecrets) devCode = code;
|
|
1629
1641
|
|
|
1642
|
+
const codeHash = await hashOtpSecret(code, getUserSecret(c.env));
|
|
1643
|
+
|
|
1630
1644
|
// Store OTP in KV with 5 min TTL
|
|
1631
1645
|
await c.env.KV.put(
|
|
1632
1646
|
`phone-otp:${phone}`,
|
|
1633
|
-
JSON.stringify({
|
|
1647
|
+
JSON.stringify({ codeHash, userId, attempts: 0 }),
|
|
1634
1648
|
{ expirationTtl: 300 },
|
|
1635
1649
|
);
|
|
1636
1650
|
|
|
@@ -1679,10 +1693,12 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1679
1693
|
const code = generateOTP();
|
|
1680
1694
|
if (exposeTestSecrets) devCode = code;
|
|
1681
1695
|
|
|
1696
|
+
const codeHash = await hashOtpSecret(code, getUserSecret(c.env));
|
|
1697
|
+
|
|
1682
1698
|
// Store OTP in KV
|
|
1683
1699
|
await c.env.KV.put(
|
|
1684
1700
|
`phone-otp:${phone}`,
|
|
1685
|
-
JSON.stringify({
|
|
1701
|
+
JSON.stringify({ codeHash, userId, attempts: 0 }),
|
|
1686
1702
|
{ expirationTtl: 300 },
|
|
1687
1703
|
);
|
|
1688
1704
|
|
|
@@ -1756,7 +1772,7 @@ authRoute.openapi(verifyPhone, async (c) => {
|
|
|
1756
1772
|
|
|
1757
1773
|
// Look up phone → userId via KV OTP data
|
|
1758
1774
|
const otpData = await c.env.KV.get(`phone-otp:${phone}`, 'json') as {
|
|
1759
|
-
code
|
|
1775
|
+
codeHash?: string; code?: string; userId: string; attempts: number;
|
|
1760
1776
|
} | null;
|
|
1761
1777
|
|
|
1762
1778
|
if (!otpData) {
|
|
@@ -1769,8 +1785,11 @@ authRoute.openapi(verifyPhone, async (c) => {
|
|
|
1769
1785
|
throw new EdgeBaseError(429, 'Too many failed OTP attempts. Please request a new code.', undefined, 'rate-limited');
|
|
1770
1786
|
}
|
|
1771
1787
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1788
|
+
const submittedCode = body.code.trim();
|
|
1789
|
+
const otpMatches = otpData.codeHash
|
|
1790
|
+
? await verifyOtpSecret(submittedCode, otpData.codeHash, getUserSecret(c.env))
|
|
1791
|
+
: timingSafeEqual(otpData.code ?? '', submittedCode);
|
|
1792
|
+
if (!otpMatches) {
|
|
1774
1793
|
await c.env.KV.put(
|
|
1775
1794
|
`phone-otp:${phone}`,
|
|
1776
1795
|
JSON.stringify({ ...otpData, attempts: otpData.attempts + 1 }),
|
|
@@ -1895,10 +1914,12 @@ authRoute.openapi(linkPhone, async (c) => {
|
|
|
1895
1914
|
const code = generateOTP();
|
|
1896
1915
|
const exposeTestSecrets = shouldExposeAuthTestSecrets(c.env);
|
|
1897
1916
|
|
|
1917
|
+
const codeHash = await hashOtpSecret(code, getUserSecret(c.env));
|
|
1918
|
+
|
|
1898
1919
|
// Store link OTP in KV (separate key pattern)
|
|
1899
1920
|
await c.env.KV.put(
|
|
1900
1921
|
`phone-link-otp:${phone}`,
|
|
1901
|
-
JSON.stringify({
|
|
1922
|
+
JSON.stringify({ codeHash, userId, attempts: 0 }),
|
|
1902
1923
|
{ expirationTtl: 300 },
|
|
1903
1924
|
);
|
|
1904
1925
|
|
|
@@ -1964,7 +1985,7 @@ authRoute.openapi(verifyLinkPhone, async (c) => {
|
|
|
1964
1985
|
|
|
1965
1986
|
// Verify OTP from KV
|
|
1966
1987
|
const otpData = await c.env.KV.get(`phone-link-otp:${phone}`, 'json') as {
|
|
1967
|
-
code
|
|
1988
|
+
codeHash?: string; code?: string; userId: string; attempts: number;
|
|
1968
1989
|
} | null;
|
|
1969
1990
|
|
|
1970
1991
|
if (!otpData) {
|
|
@@ -1980,7 +2001,11 @@ authRoute.openapi(verifyLinkPhone, async (c) => {
|
|
|
1980
2001
|
throw new EdgeBaseError(429, 'Too many failed OTP attempts. Please request a new code.', undefined, 'rate-limited');
|
|
1981
2002
|
}
|
|
1982
2003
|
|
|
1983
|
-
|
|
2004
|
+
const submittedCode = body.code.trim();
|
|
2005
|
+
const otpMatches = otpData.codeHash
|
|
2006
|
+
? await verifyOtpSecret(submittedCode, otpData.codeHash, getUserSecret(c.env))
|
|
2007
|
+
: timingSafeEqual(otpData.code ?? '', submittedCode);
|
|
2008
|
+
if (!otpMatches) {
|
|
1984
2009
|
await c.env.KV.put(
|
|
1985
2010
|
`phone-link-otp:${phone}`,
|
|
1986
2011
|
JSON.stringify({ ...otpData, attempts: otpData.attempts + 1 }),
|
|
@@ -2083,11 +2108,12 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2083
2108
|
|
|
2084
2109
|
const code = generateOTP();
|
|
2085
2110
|
if (exposeTestSecrets) devCode = code;
|
|
2111
|
+
const codeHash = await hashOtpSecret(code, getUserSecret(c.env));
|
|
2086
2112
|
|
|
2087
2113
|
// Store OTP in KV with 5 min TTL
|
|
2088
2114
|
await c.env.KV.put(
|
|
2089
2115
|
`email-otp:${email}`,
|
|
2090
|
-
JSON.stringify({
|
|
2116
|
+
JSON.stringify({ codeHash, userId, attempts: 0 }),
|
|
2091
2117
|
{ expirationTtl: 300 },
|
|
2092
2118
|
);
|
|
2093
2119
|
|
|
@@ -2141,11 +2167,12 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2141
2167
|
|
|
2142
2168
|
const code = generateOTP();
|
|
2143
2169
|
if (exposeTestSecrets) devCode = code;
|
|
2170
|
+
const codeHash = await hashOtpSecret(code, getUserSecret(c.env));
|
|
2144
2171
|
|
|
2145
2172
|
// Store OTP in KV
|
|
2146
2173
|
await c.env.KV.put(
|
|
2147
2174
|
`email-otp:${email}`,
|
|
2148
|
-
JSON.stringify({
|
|
2175
|
+
JSON.stringify({ codeHash, userId, attempts: 0 }),
|
|
2149
2176
|
{ expirationTtl: 300 },
|
|
2150
2177
|
);
|
|
2151
2178
|
|
|
@@ -2218,7 +2245,7 @@ authRoute.openapi(verifyEmailOtp, async (c) => {
|
|
|
2218
2245
|
|
|
2219
2246
|
// Look up OTP data from KV
|
|
2220
2247
|
const otpData = await c.env.KV.get(`email-otp:${email}`, 'json') as {
|
|
2221
|
-
code
|
|
2248
|
+
codeHash?: string; code?: string; userId: string; attempts: number;
|
|
2222
2249
|
} | null;
|
|
2223
2250
|
|
|
2224
2251
|
if (!otpData) {
|
|
@@ -2230,7 +2257,11 @@ authRoute.openapi(verifyEmailOtp, async (c) => {
|
|
|
2230
2257
|
throw new EdgeBaseError(429, 'Too many failed OTP attempts. Please request a new code.', undefined, 'rate-limited');
|
|
2231
2258
|
}
|
|
2232
2259
|
|
|
2233
|
-
|
|
2260
|
+
const submittedCode = body.code.trim();
|
|
2261
|
+
const otpMatches = otpData.codeHash
|
|
2262
|
+
? await verifyOtpSecret(submittedCode, otpData.codeHash, getUserSecret(c.env))
|
|
2263
|
+
: timingSafeEqual(otpData.code ?? '', submittedCode);
|
|
2264
|
+
if (!otpMatches) {
|
|
2234
2265
|
await c.env.KV.put(
|
|
2235
2266
|
`email-otp:${email}`,
|
|
2236
2267
|
JSON.stringify({ ...otpData, attempts: otpData.attempts + 1 }),
|
|
@@ -2577,6 +2608,67 @@ authRoute.openapi(mfaRecovery, async (c) => {
|
|
|
2577
2608
|
});
|
|
2578
2609
|
});
|
|
2579
2610
|
|
|
2611
|
+
// POST /mfa/recovery-codes — regenerate recovery codes (authenticated)
|
|
2612
|
+
const mfaRecoveryCodesRegenerate = createRoute({
|
|
2613
|
+
operationId: 'authMfaRecoveryCodesRegenerate',
|
|
2614
|
+
method: 'post',
|
|
2615
|
+
path: '/mfa/recovery-codes',
|
|
2616
|
+
tags: ['client'],
|
|
2617
|
+
summary: 'Regenerate MFA recovery codes',
|
|
2618
|
+
request: {
|
|
2619
|
+
body: { content: { 'application/json': { schema: z.object({
|
|
2620
|
+
password: z.string().optional(),
|
|
2621
|
+
code: z.string().optional(),
|
|
2622
|
+
}).passthrough() } }, required: false },
|
|
2623
|
+
},
|
|
2624
|
+
responses: {
|
|
2625
|
+
200: { description: 'Success', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
2626
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
2627
|
+
401: { description: 'Authentication required or verification failed', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
2628
|
+
},
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
authRoute.openapi(mfaRecoveryCodesRegenerate, async (c) => {
|
|
2632
|
+
const userId = requireAuth(c.get('auth'));
|
|
2633
|
+
const bodyText = await c.req.text();
|
|
2634
|
+
let body: { password?: string; code?: string } = {};
|
|
2635
|
+
try { body = bodyText ? JSON.parse(bodyText) : {}; } catch { throw new EdgeBaseError(400, 'Invalid JSON.', undefined, 'invalid-json'); }
|
|
2636
|
+
await ensureAuthActionAllowed(c, 'mfaRecoveryCodesRegenerate', {
|
|
2637
|
+
userId,
|
|
2638
|
+
passwordProvided: !!body.password,
|
|
2639
|
+
codeProvided: !!body.code,
|
|
2640
|
+
});
|
|
2641
|
+
const db = getAuthDb(c);
|
|
2642
|
+
|
|
2643
|
+
const user = await authService.getUserById(db, userId);
|
|
2644
|
+
if (!user) throw new EdgeBaseError(404, 'User not found.', undefined, 'user-not-found');
|
|
2645
|
+
const factor = await authService.getMfaFactorByUser(db, userId, 'totp');
|
|
2646
|
+
if (!factor || !factor.verified) {
|
|
2647
|
+
throw new EdgeBaseError(400, 'No verified TOTP factor found.', undefined, 'invalid-input');
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
if (body.password) {
|
|
2651
|
+
if (!user.passwordHash) throw new EdgeBaseError(400, 'This account has no password set.', undefined, 'invalid-input');
|
|
2652
|
+
const valid = await verifyPassword(body.password, user.passwordHash as string);
|
|
2653
|
+
if (!valid) throw new EdgeBaseError(401, 'Invalid password.', undefined, 'invalid-password');
|
|
2654
|
+
} else if (body.code) {
|
|
2655
|
+
const secret = await decryptSecret(factor.secret as string, getUserSecret(c.env));
|
|
2656
|
+
const valid = await verifyTOTP(secret, body.code);
|
|
2657
|
+
if (!valid) throw new EdgeBaseError(401, 'Invalid TOTP code.', undefined, 'invalid-totp');
|
|
2658
|
+
} else {
|
|
2659
|
+
throw new EdgeBaseError(400, 'Either password or TOTP code is required to regenerate recovery codes.', undefined, 'invalid-input');
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const recoveryCodes = generateRecoveryCodes(8);
|
|
2663
|
+
const hashedCodes: { id: string; codeHash: string }[] = [];
|
|
2664
|
+
for (const code of recoveryCodes) {
|
|
2665
|
+
hashedCodes.push({ id: generateId(), codeHash: await hashRecoveryCode(code) });
|
|
2666
|
+
}
|
|
2667
|
+
await authService.replaceRecoveryCodes(db, userId, hashedCodes);
|
|
2668
|
+
|
|
2669
|
+
return c.json({ recoveryCodes });
|
|
2670
|
+
});
|
|
2671
|
+
|
|
2580
2672
|
// DELETE /mfa/totp — disable TOTP (authenticated)
|
|
2581
2673
|
const mfaTotpDelete = createRoute({
|
|
2582
2674
|
operationId: 'authMfaTotpDelete',
|
package/src/routes/room.ts
CHANGED
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
* - KV counter with expirationTtl: 60s
|
|
13
13
|
*/
|
|
14
14
|
import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
|
|
15
|
-
import type { RoomNamespaceConfig } from '@edge-base/shared';
|
|
15
|
+
import type { EdgeBaseConfig, RoomHandlerContext, RoomNamespaceConfig } from '@edge-base/shared';
|
|
16
16
|
import type { Context } from 'hono';
|
|
17
17
|
import { parseConfig } from '../lib/do-router.js';
|
|
18
|
+
import { buildFunctionContext } from '../lib/functions.js';
|
|
18
19
|
import { resolveRoomRuntime } from '../lib/room-runtime.js';
|
|
19
20
|
import { zodDefaultHook, jsonResponseSchema, errorResponseSchema } from '../lib/schemas.js';
|
|
21
|
+
import { resolveRootServiceKey } from '../lib/service-key.js';
|
|
20
22
|
import {
|
|
21
23
|
acquirePendingWebSocketSlot,
|
|
22
24
|
getPendingWebSocketCount,
|
|
@@ -111,6 +113,33 @@ function getRoomAuthContext(
|
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
function buildRoomRouteAccessContext(
|
|
117
|
+
c: Context<HonoEnv>,
|
|
118
|
+
config: EdgeBaseConfig,
|
|
119
|
+
): RoomHandlerContext {
|
|
120
|
+
let executionCtx: ExecutionContext | undefined;
|
|
121
|
+
try {
|
|
122
|
+
executionCtx = c.executionCtx;
|
|
123
|
+
} catch {
|
|
124
|
+
executionCtx = undefined;
|
|
125
|
+
}
|
|
126
|
+
const ctx = buildFunctionContext({
|
|
127
|
+
request: c.req.raw,
|
|
128
|
+
auth: getRoomAuthContext(c.get('auth') as never),
|
|
129
|
+
databaseNamespace: c.env.DATABASE,
|
|
130
|
+
authNamespace: c.env.AUTH,
|
|
131
|
+
d1Database: c.env.AUTH_DB,
|
|
132
|
+
kvNamespace: c.env.KV,
|
|
133
|
+
config,
|
|
134
|
+
env: c.env,
|
|
135
|
+
executionCtx,
|
|
136
|
+
serviceKey: resolveRootServiceKey(config, c.env),
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
admin: ctx.admin as never,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
export async function proxyRoomDoRequest(
|
|
115
144
|
c: Context<HonoEnv>,
|
|
116
145
|
path: string,
|
|
@@ -433,9 +462,11 @@ roomRoute.openapi(getRoomMetadata, async (c) => {
|
|
|
433
462
|
}
|
|
434
463
|
if (metadataAccess) {
|
|
435
464
|
const auth = (c.get('auth') as { id?: string; role?: string; email?: string | null; custom?: Record<string, unknown> | null; isAnonymous?: boolean; meta?: Record<string, unknown> } | null | undefined) ?? null;
|
|
465
|
+
const accessCtx = buildRoomRouteAccessContext(c, config);
|
|
436
466
|
const allowed = await Promise.resolve(metadataAccess(
|
|
437
467
|
getRoomAuthContext(auth),
|
|
438
468
|
roomId,
|
|
469
|
+
accessCtx,
|
|
439
470
|
)).catch(() => false);
|
|
440
471
|
if (!allowed) {
|
|
441
472
|
return c.json({ code: 403, message: 'Denied by room metadata access rule' }, 403);
|
|
@@ -493,9 +524,11 @@ roomRoute.openapi(getRoomSummary, async (c) => {
|
|
|
493
524
|
}
|
|
494
525
|
if (metadataAccess) {
|
|
495
526
|
const auth = (c.get('auth') as { id?: string; role?: string; email?: string | null; custom?: Record<string, unknown> | null; isAnonymous?: boolean; meta?: Record<string, unknown> } | null | undefined) ?? null;
|
|
527
|
+
const accessCtx = buildRoomRouteAccessContext(c, config);
|
|
496
528
|
const allowed = await Promise.resolve(metadataAccess(
|
|
497
529
|
getRoomAuthContext(auth),
|
|
498
530
|
roomId,
|
|
531
|
+
accessCtx,
|
|
499
532
|
)).catch(() => false);
|
|
500
533
|
if (!allowed) {
|
|
501
534
|
return c.json({ code: 403, message: 'Denied by room metadata access rule' }, 403);
|
|
@@ -557,8 +590,9 @@ roomRoute.openapi(getRoomSummaries, async (c) => {
|
|
|
557
590
|
const deniedIds: string[] = [];
|
|
558
591
|
|
|
559
592
|
if (metadataAccess) {
|
|
593
|
+
const accessCtx = buildRoomRouteAccessContext(c, config);
|
|
560
594
|
for (const roomId of roomIds) {
|
|
561
|
-
const allowed = await Promise.resolve(metadataAccess(authContext, roomId)).catch(() => false);
|
|
595
|
+
const allowed = await Promise.resolve(metadataAccess(authContext, roomId, accessCtx)).catch(() => false);
|
|
562
596
|
if (allowed) {
|
|
563
597
|
allowedRoomIds.push(roomId);
|
|
564
598
|
} else {
|