@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28
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/README.md +3 -5
- package/dist/bin.js +6488 -1571
- package/dist/browser/index.js +10 -7
- package/dist/browser/locks.js +3 -5
- package/dist/browser/navigation.js +7 -10
- package/dist/browser/runtime.js +35 -33
- package/dist/client/core/types.js +17 -0
- package/dist/client/factors/device.js +26 -19
- package/dist/client/index.js +151 -163
- package/dist/client/runtime/proxy.js +6 -6
- package/dist/client/services/adapters.js +3 -7
- package/dist/client/services/http.js +2 -5
- package/dist/client/services/resolve.js +5 -11
- package/dist/client/services/runtime.js +2 -5
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/index.d.ts +3 -3
- package/dist/component/model.d.ts +25 -25
- package/dist/component/public/identity/sessions.js +38 -1
- package/dist/component/public/identity/tokens.js +81 -3
- package/dist/component/public/identity/verifiers.js +9 -3
- package/dist/component/public.js +3 -3
- package/dist/component/schema.d.ts +320 -320
- package/dist/core/index.d.ts +380 -0
- package/dist/core/index.js +83 -0
- package/dist/otel.d.ts +13 -17
- package/dist/otel.js +39 -49
- package/dist/providers/email.d.ts +2 -2
- package/dist/providers/password.js +8 -16
- package/dist/providers/phone.js +2 -9
- package/dist/server/auth-context.d.ts +204 -0
- package/dist/server/auth-context.js +76 -0
- package/dist/server/auth.d.ts +25 -187
- package/dist/server/auth.js +5 -96
- package/dist/server/componentContext.d.ts +12 -0
- package/dist/server/componentContext.js +1 -0
- package/dist/server/config.js +1 -12
- package/dist/server/constants.js +6 -0
- package/dist/server/contract.d.ts +1 -1
- package/dist/server/core.js +5 -14
- package/dist/server/crypto.js +26 -18
- package/dist/server/db.js +6 -1
- package/dist/server/device.js +88 -78
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.js +74 -86
- package/dist/server/index.d.ts +2 -1
- package/dist/server/limits.js +22 -15
- package/dist/server/mounts.d.ts +103 -103
- package/dist/server/mutations/account.js +6 -4
- package/dist/server/mutations/invalidate.js +3 -6
- package/dist/server/mutations/oauth.js +86 -88
- package/dist/server/mutations/refresh.js +45 -87
- package/dist/server/mutations/register.js +19 -19
- package/dist/server/mutations/retrieve.js +17 -15
- package/dist/server/mutations/signature.js +9 -13
- package/dist/server/mutations/signin.js +7 -3
- package/dist/server/mutations/signout.js +10 -15
- package/dist/server/mutations/store.js +22 -12
- package/dist/server/mutations/verifier.js +11 -6
- package/dist/server/mutations/verify.js +55 -46
- package/dist/server/oauth/runtime.js +27 -25
- package/dist/server/passkey.js +299 -250
- package/dist/server/prefetch.js +283 -281
- package/dist/server/refresh.js +7 -60
- package/dist/server/runtime.d.ts +82 -206
- package/dist/server/runtime.js +63 -56
- package/dist/server/services/config.js +5 -3
- package/dist/server/services/logger.js +2 -4
- package/dist/server/services/providers.js +2 -4
- package/dist/server/services/refresh.js +2 -4
- package/dist/server/services/resolve.js +15 -14
- package/dist/server/services/signin.js +2 -4
- package/dist/server/sessions.js +32 -33
- package/dist/server/signin.js +177 -142
- package/dist/server/sso/domain.d.ts +20 -68
- package/dist/server/sso/domain.js +444 -413
- package/dist/server/sso/http.js +53 -59
- package/dist/server/sso/oidc.js +94 -80
- package/dist/server/tokens.js +13 -3
- package/dist/server/totp.js +153 -116
- package/dist/server/types.d.ts +2 -2
- package/dist/server/users.js +18 -23
- package/dist/server/utils/cache.js +51 -0
- package/dist/server/utils/dispatch.js +36 -0
- package/dist/server/utils/retry.js +24 -0
- package/dist/server/utils/span.js +32 -0
- package/dist/shared/errors.js +9 -3
- package/dist/shared/log.js +20 -22
- package/package.json +41 -33
package/dist/server/prefetch.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { isLocalHost } from "./url.js";
|
|
2
1
|
import { log } from "./log.js";
|
|
2
|
+
import { isLocalHost } from "./url.js";
|
|
3
3
|
import { ConvexError } from "convex/values";
|
|
4
|
-
import { Cause, Effect, Exit, Match } from "effect";
|
|
5
4
|
import { ConvexHttpClient } from "convex/browser";
|
|
6
5
|
import { makeFunctionReference } from "convex/server";
|
|
7
6
|
import { parse, serialize } from "cookie";
|
|
@@ -156,20 +155,15 @@ function shouldProxyAuthAction(pathname, apiRoute) {
|
|
|
156
155
|
if (apiRoute.endsWith("/")) return pathname === apiRoute || pathname === apiRoute.slice(0, -1);
|
|
157
156
|
return pathname === apiRoute || pathname === `${apiRoute}/`;
|
|
158
157
|
}
|
|
159
|
-
const REQUIRED_TOKEN_LIFETIME_MS =
|
|
160
|
-
const MINIMUM_REQUIRED_TOKEN_LIFETIME_MS =
|
|
158
|
+
const REQUIRED_TOKEN_LIFETIME_MS = 3e4;
|
|
159
|
+
const MINIMUM_REQUIRED_TOKEN_LIFETIME_MS = 5e3;
|
|
161
160
|
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
162
|
-
function runBoundary(effect) {
|
|
163
|
-
return Effect.runPromiseExit(effect).then(Exit.match({
|
|
164
|
-
onSuccess: (value) => value,
|
|
165
|
-
onFailure: (cause) => Promise.reject(Cause.squash(cause))
|
|
166
|
-
}));
|
|
167
|
-
}
|
|
168
161
|
function decodeToken(token) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
162
|
+
try {
|
|
163
|
+
return jwtDecode(token);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
173
167
|
}
|
|
174
168
|
function jsonResponse(body, status = 200) {
|
|
175
169
|
return new Response(JSON.stringify(body), {
|
|
@@ -191,7 +185,8 @@ function getProxyErrorBody(error) {
|
|
|
191
185
|
} : { error: error instanceof Error ? error.message : String(error) };
|
|
192
186
|
}
|
|
193
187
|
function extractSignedInTokens(result, context) {
|
|
194
|
-
|
|
188
|
+
if (result.kind === "signedIn") return result.tokens;
|
|
189
|
+
throw new Error(`Invalid \`auth:signIn\` result for ${context}`);
|
|
195
190
|
}
|
|
196
191
|
function normalizeCookieNamespace(cookieNamespace) {
|
|
197
192
|
if (cookieNamespace === void 0 || cookieNamespace === null) return null;
|
|
@@ -302,12 +297,10 @@ function server(options) {
|
|
|
302
297
|
return parseAuthCookies(request.headers.get("cookie"), request.headers.get("host") ?? new URL(request.url).host, cookieNamespace).token;
|
|
303
298
|
},
|
|
304
299
|
async verify(request) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now();
|
|
310
|
-
}).pipe(Effect.withSpan("convex-auth.ssr.refresh")));
|
|
300
|
+
const token = parseAuthCookies(request.headers.get("cookie"), request.headers.get("host") ?? new URL(request.url).host, cookieNamespace).token;
|
|
301
|
+
if (token === null) return false;
|
|
302
|
+
const decodedToken = decodeToken(token);
|
|
303
|
+
return decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now();
|
|
311
304
|
},
|
|
312
305
|
async proxy(request) {
|
|
313
306
|
const createClient = (token) => {
|
|
@@ -315,184 +308,85 @@ function server(options) {
|
|
|
315
308
|
if (token !== null && token !== void 0) client.setAuth(token);
|
|
316
309
|
return client;
|
|
317
310
|
};
|
|
318
|
-
const runSignIn = (client, args) =>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
322
|
-
const runSignOut = (token) => Effect.tryPromise({
|
|
323
|
-
try: () => createClient(token).action(signOutActionRef),
|
|
324
|
-
catch: (error) => error
|
|
325
|
-
});
|
|
326
|
-
const hydrateProxySignInClient = (currentCookies, args) => Effect.gen(function* () {
|
|
311
|
+
const runSignIn = async (client, args$1) => client.action(signInActionRef, args$1);
|
|
312
|
+
const runSignOut = async (token) => createClient(token).action(signOutActionRef);
|
|
313
|
+
const hydrateProxySignInClient = async (currentCookies$1, args$1) => {
|
|
327
314
|
const client = createClient();
|
|
328
|
-
const requestParams = typeof args.params === "object" && args.params !== null ? args.params : void 0;
|
|
329
|
-
if (!(args.refreshToken === void 0 && requestParams?.code === void 0)) return {
|
|
315
|
+
const requestParams = typeof args$1.params === "object" && args$1.params !== null ? args$1.params : void 0;
|
|
316
|
+
if (!(args$1.refreshToken === void 0 && requestParams?.code === void 0)) return {
|
|
330
317
|
client,
|
|
331
|
-
cookies: currentCookies
|
|
318
|
+
cookies: currentCookies$1
|
|
332
319
|
};
|
|
333
|
-
const currentToken = currentCookies.token;
|
|
334
|
-
const
|
|
335
|
-
if (currentToken !== null &&
|
|
336
|
-
|
|
337
|
-
client.setAuth(currentToken);
|
|
338
|
-
});
|
|
320
|
+
const currentToken = currentCookies$1.token;
|
|
321
|
+
const decodedTokenValue = currentToken === null ? null : decodeToken(currentToken);
|
|
322
|
+
if (currentToken !== null && decodedTokenValue?.exp !== void 0 && decodedTokenValue.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedTokenValue.iss)) && decodedTokenValue.exp * 1e3 > Date.now()) {
|
|
323
|
+
client.setAuth(currentToken);
|
|
339
324
|
return {
|
|
340
325
|
client,
|
|
341
|
-
cookies: currentCookies
|
|
326
|
+
cookies: currentCookies$1
|
|
342
327
|
};
|
|
343
328
|
}
|
|
344
|
-
if (currentCookies.refreshToken === null) return {
|
|
329
|
+
if (currentCookies$1.refreshToken === null) return {
|
|
345
330
|
client,
|
|
346
|
-
cookies: currentCookies
|
|
331
|
+
cookies: currentCookies$1
|
|
347
332
|
};
|
|
348
|
-
|
|
333
|
+
let refreshedTokens = null;
|
|
334
|
+
try {
|
|
335
|
+
refreshedTokens = extractSignedInTokens(await runSignIn(createClient(), { refreshToken: currentCookies$1.refreshToken }), "proxy sign-in auth hydration");
|
|
336
|
+
} catch {
|
|
337
|
+
refreshedTokens = null;
|
|
338
|
+
}
|
|
349
339
|
if (refreshedTokens === null) return {
|
|
350
340
|
client,
|
|
351
|
-
cookies: currentCookies
|
|
341
|
+
cookies: currentCookies$1
|
|
352
342
|
};
|
|
353
|
-
|
|
354
|
-
client.setAuth(refreshedTokens.token);
|
|
355
|
-
});
|
|
343
|
+
client.setAuth(refreshedTokens.token);
|
|
356
344
|
return {
|
|
357
345
|
client,
|
|
358
346
|
cookies: {
|
|
359
347
|
token: refreshedTokens.token,
|
|
360
348
|
refreshToken: refreshedTokens.refreshToken,
|
|
361
|
-
verifier: currentCookies.verifier
|
|
349
|
+
verifier: currentCookies$1.verifier
|
|
362
350
|
}
|
|
363
351
|
};
|
|
364
|
-
}
|
|
365
|
-
const toSignInProxyResponse = (result, args, currentCookies, host) =>
|
|
366
|
-
kind
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
token: signedInResult.tokens.token,
|
|
379
|
-
refreshToken: signedInResult.tokens.refreshToken,
|
|
380
|
-
verifier: null
|
|
381
|
-
};
|
|
382
|
-
return appendCookieHeaders(jsonResponse({
|
|
383
|
-
kind: "signedIn",
|
|
384
|
-
tokens: signedInResult.tokens === null ? null : {
|
|
385
|
-
token: signedInResult.tokens.token,
|
|
386
|
-
refreshToken: "dummy"
|
|
387
|
-
}
|
|
388
|
-
}), serializeAuthCookies(nextCookies, host, cookieConfig, cookieNamespace));
|
|
389
|
-
})), Match.when({ kind: "started" }, (startedResult) => Effect.succeed(jsonResponse(startedResult))), Match.when({ kind: "passkeyOptions" }, (passkeyOptionsResult) => Effect.succeed(jsonResponse(passkeyOptionsResult))), Match.when({ kind: "totpRequired" }, (totpRequiredResult) => Effect.succeed(jsonResponse(totpRequiredResult))), Match.when({ kind: "totpSetup" }, (totpSetupResult) => Effect.succeed(jsonResponse(totpSetupResult))), Match.when({ kind: "deviceCode" }, (deviceCodeResult) => Effect.succeed(jsonResponse(deviceCodeResult))), Match.exhaustive).pipe(Effect.catch((error) => Effect.sync(() => {
|
|
390
|
-
const response = jsonResponse(getProxyErrorBody(error), 400);
|
|
391
|
-
const clearSession = args.refreshToken !== void 0 && getConvexErrorCode(error) === "INVALID_REFRESH_TOKEN";
|
|
392
|
-
return appendCookieHeaders(response, serializeAuthCookies({
|
|
393
|
-
token: clearSession ? null : currentCookies.token,
|
|
394
|
-
refreshToken: clearSession ? null : currentCookies.refreshToken,
|
|
395
|
-
verifier: null
|
|
396
|
-
}, host, cookieConfig, cookieNamespace));
|
|
397
|
-
})));
|
|
398
|
-
return runBoundary(Effect.gen(function* () {
|
|
399
|
-
const requestDispatch = !shouldProxyAuthAction(new URL(request.url).pathname, apiRoute) ? { kind: "invalidRoute" } : request.method !== "POST" ? { kind: "invalidMethod" } : (() => {
|
|
400
|
-
const originHeader = request.headers.get("origin");
|
|
401
|
-
if (originHeader === null) return false;
|
|
402
|
-
const forwardedProtoHeader = request.headers.get("x-forwarded-proto");
|
|
403
|
-
const protocol = forwardedProtoHeader !== null ? (() => {
|
|
404
|
-
const forwardedProto = forwardedProtoHeader.split(",")[0]?.trim();
|
|
405
|
-
if (forwardedProto !== void 0 && forwardedProto.length > 0) return forwardedProto.endsWith(":") ? forwardedProto : `${forwardedProto}:`;
|
|
406
|
-
return new URL(request.url).protocol;
|
|
407
|
-
})() : new URL(request.url).protocol;
|
|
408
|
-
const requestHost = request.headers.get("host") ?? new URL(request.url).host;
|
|
409
|
-
const hostCandidate = `${protocol}//${requestHost}`;
|
|
410
|
-
const host$1 = canParseUrl(hostCandidate) ? new URL(hostCandidate).host : requestHost;
|
|
411
|
-
if (!canParseUrl(originHeader)) return true;
|
|
412
|
-
const originUrl = new URL(originHeader);
|
|
413
|
-
return originUrl.host !== host$1 || originUrl.protocol !== protocol;
|
|
414
|
-
})() ? { kind: "invalidOrigin" } : { kind: "valid" };
|
|
415
|
-
const validationErrorResponse = Match.value(requestDispatch).pipe(Match.when({ kind: "invalidRoute" }, () => new Response("Invalid route", { status: 404 })), Match.when({ kind: "invalidMethod" }, () => new Response("Invalid method", { status: 405 })), Match.when({ kind: "invalidOrigin" }, () => new Response("Invalid origin", { status: 403 })), Match.when({ kind: "valid" }, () => null), Match.exhaustive);
|
|
416
|
-
if (validationErrorResponse !== null) return validationErrorResponse;
|
|
417
|
-
const body = yield* Effect.tryPromise({
|
|
418
|
-
try: () => request.json(),
|
|
419
|
-
catch: () => null
|
|
420
|
-
}).pipe(Effect.catch(() => Effect.succeed(null)), Effect.map((parsed) => typeof parsed === "object" && parsed !== null ? parsed : null));
|
|
421
|
-
if (body === null) return new Response("Invalid request body", { status: 400 });
|
|
422
|
-
const action = body.action;
|
|
423
|
-
const args = typeof body.args === "object" && body.args !== null ? { ...body.args } : {};
|
|
424
|
-
if (args.refreshToken === null) args.refreshToken = void 0;
|
|
425
|
-
const actionDispatch = action === "auth:signIn" ? { action: "sessionStart" } : action === "auth:signOut" ? { action: "sessionStop" } : null;
|
|
426
|
-
if (actionDispatch === null) return new Response("Invalid action", { status: 400 });
|
|
427
|
-
const host = request.headers.get("host") ?? new URL(request.url).host;
|
|
428
|
-
const currentCookies = parseAuthCookies(request.headers.get("cookie"), host, cookieNamespace);
|
|
429
|
-
return yield* Match.value(actionDispatch).pipe(Match.when({ action: "sessionStart" }, () => Effect.gen(function* () {
|
|
430
|
-
const refreshResponse = yield* Match.value({
|
|
431
|
-
kind: args.refreshToken === void 0 ? "passthrough" : currentCookies.refreshToken === null ? "refreshRequestedWithoutCookie" : "hydrateRefreshFromCookie",
|
|
432
|
-
refreshToken: currentCookies.refreshToken
|
|
433
|
-
}).pipe(Match.when({ kind: "passthrough" }, () => Effect.succeed(null)), Match.when({ kind: "hydrateRefreshFromCookie" }, ({ refreshToken }) => Effect.sync(() => {
|
|
434
|
-
args.refreshToken = refreshToken ?? void 0;
|
|
435
|
-
return null;
|
|
436
|
-
})), Match.when({ kind: "refreshRequestedWithoutCookie" }, () => Effect.gen(function* () {
|
|
437
|
-
const currentToken = currentCookies.token;
|
|
438
|
-
const decodedToken = currentToken === null ? null : yield* decodeToken(currentToken);
|
|
439
|
-
return Match.value(currentToken !== null && decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now() ? {
|
|
440
|
-
kind: "validToken",
|
|
441
|
-
token: currentToken
|
|
442
|
-
} : { kind: "missingToken" }).pipe(Match.when({ kind: "validToken" }, ({ token }) => jsonResponse({ tokens: {
|
|
443
|
-
token,
|
|
444
|
-
refreshToken: "dummy"
|
|
445
|
-
} })), Match.when({ kind: "missingToken" }, () => jsonResponse({ tokens: null })), Match.exhaustive);
|
|
446
|
-
})), Match.exhaustive);
|
|
447
|
-
if (refreshResponse !== null) return refreshResponse;
|
|
448
|
-
const { client, cookies: effectiveCookies } = yield* hydrateProxySignInClient(currentCookies, args);
|
|
449
|
-
return yield* runSignIn(client, args).pipe(Effect.flatMap((result) => toSignInProxyResponse(result, args, effectiveCookies, host)), Effect.catch((error) => Effect.succeed(appendCookieHeaders(jsonResponse(getProxyErrorBody(error), 400), serializeAuthCookies({
|
|
450
|
-
token: effectiveCookies.token,
|
|
451
|
-
refreshToken: effectiveCookies.refreshToken,
|
|
352
|
+
};
|
|
353
|
+
const toSignInProxyResponse = (result, args$1, currentCookies$1, host$1) => {
|
|
354
|
+
if (result.kind === "redirect") return appendCookieHeaders(jsonResponse({
|
|
355
|
+
kind: "redirect",
|
|
356
|
+
redirect: result.redirect,
|
|
357
|
+
verifier: result.verifier
|
|
358
|
+
}), serializeAuthCookies({
|
|
359
|
+
...currentCookies$1,
|
|
360
|
+
verifier: result.verifier
|
|
361
|
+
}, host$1, cookieConfig, cookieNamespace));
|
|
362
|
+
if (result.kind === "signedIn") {
|
|
363
|
+
const nextCookies = result.tokens === null ? {
|
|
364
|
+
token: currentCookies$1.token,
|
|
365
|
+
refreshToken: currentCookies$1.refreshToken,
|
|
452
366
|
verifier: null
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
yield* Effect.sync(() => {
|
|
457
|
-
log("ERROR", "[convex-auth/server] proxy sign-out failed", error);
|
|
458
|
-
});
|
|
459
|
-
yield* Match.value(currentCookies.refreshToken).pipe(Match.when(null, () => Effect.void), Match.orElse((refreshToken) => Effect.gen(function* () {
|
|
460
|
-
yield* runSignIn(createClient(), { refreshToken }).pipe(Effect.flatMap((refreshed) => extractSignedInTokens(refreshed, "sign-out fallback refresh")), Effect.flatMap((refreshedTokens) => refreshedTokens !== null ? runSignOut(refreshedTokens.token) : Effect.void), Effect.catch((fallbackError) => Effect.sync(() => {
|
|
461
|
-
log("ERROR", "[convex-auth/server] proxy sign-out fallback failed", fallbackError);
|
|
462
|
-
})));
|
|
463
|
-
})));
|
|
464
|
-
})));
|
|
465
|
-
return appendCookieHeaders(jsonResponse(null), serializeAuthCookies({
|
|
466
|
-
token: null,
|
|
467
|
-
refreshToken: null,
|
|
367
|
+
} : {
|
|
368
|
+
token: result.tokens.token,
|
|
369
|
+
refreshToken: result.tokens.refreshToken,
|
|
468
370
|
verifier: null
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}) : Effect.void;
|
|
478
|
-
const refreshWithToken = (refreshToken) => Effect.tryPromise({
|
|
479
|
-
try: () => createClient().action(signInActionRef, { refreshToken }),
|
|
480
|
-
catch: (error) => error
|
|
481
|
-
}).pipe(Effect.flatMap((result) => extractSignedInTokens(result, "token refresh")), Effect.tap((tokens) => logVerbose(`Refreshed tokens, null=${tokens === null}`)), Effect.catch((error) => Effect.gen(function* () {
|
|
482
|
-
yield* Effect.sync(() => {
|
|
483
|
-
log("ERROR", "[convex-auth/server] refresh-token exchange failed", error);
|
|
484
|
-
});
|
|
485
|
-
if (getConvexErrorCode(error) === "INVALID_REFRESH_TOKEN") {
|
|
486
|
-
yield* logVerbose("Refresh token rejected, clearing auth cookies");
|
|
487
|
-
return null;
|
|
371
|
+
};
|
|
372
|
+
return appendCookieHeaders(jsonResponse({
|
|
373
|
+
kind: "signedIn",
|
|
374
|
+
tokens: result.tokens === null ? null : {
|
|
375
|
+
token: result.tokens.token,
|
|
376
|
+
refreshToken: "dummy"
|
|
377
|
+
}
|
|
378
|
+
}), serializeAuthCookies(nextCookies, host$1, cookieConfig, cookieNamespace));
|
|
488
379
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
380
|
+
if (result.kind === "started") return jsonResponse(result);
|
|
381
|
+
if (result.kind === "passkeyOptions") return jsonResponse(result);
|
|
382
|
+
if (result.kind === "totpRequired") return jsonResponse(result);
|
|
383
|
+
if (result.kind === "totpSetup") return jsonResponse(result);
|
|
384
|
+
if (result.kind === "deviceCode") return jsonResponse(result);
|
|
385
|
+
return jsonResponse(result);
|
|
386
|
+
};
|
|
387
|
+
const requestDispatch = !shouldProxyAuthAction(new URL(request.url).pathname, apiRoute) ? { kind: "invalidRoute" } : request.method !== "POST" ? { kind: "invalidMethod" } : (() => {
|
|
495
388
|
const originHeader = request.headers.get("origin");
|
|
389
|
+
if (originHeader === null) return false;
|
|
496
390
|
const forwardedProtoHeader = request.headers.get("x-forwarded-proto");
|
|
497
391
|
const protocol = forwardedProtoHeader !== null ? (() => {
|
|
498
392
|
const forwardedProto = forwardedProtoHeader.split(",")[0]?.trim();
|
|
@@ -501,129 +395,237 @@ function server(options) {
|
|
|
501
395
|
})() : new URL(request.url).protocol;
|
|
502
396
|
const requestHost = request.headers.get("host") ?? new URL(request.url).host;
|
|
503
397
|
const hostCandidate = `${protocol}//${requestHost}`;
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
398
|
+
const host$1 = canParseUrl(hostCandidate) ? new URL(hostCandidate).host : requestHost;
|
|
399
|
+
if (!canParseUrl(originHeader)) return true;
|
|
400
|
+
const originUrl = new URL(originHeader);
|
|
401
|
+
return originUrl.host !== host$1 || originUrl.protocol !== protocol;
|
|
402
|
+
})() ? { kind: "invalidOrigin" } : { kind: "valid" };
|
|
403
|
+
let validationErrorResponse = null;
|
|
404
|
+
if (requestDispatch.kind === "invalidRoute") validationErrorResponse = new Response("Invalid route", { status: 404 });
|
|
405
|
+
else if (requestDispatch.kind === "invalidMethod") validationErrorResponse = new Response("Invalid method", { status: 405 });
|
|
406
|
+
else if (requestDispatch.kind === "invalidOrigin") validationErrorResponse = new Response("Invalid origin", { status: 403 });
|
|
407
|
+
if (validationErrorResponse !== null) return validationErrorResponse;
|
|
408
|
+
let body = null;
|
|
409
|
+
try {
|
|
410
|
+
const parsed = await request.json();
|
|
411
|
+
body = typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
412
|
+
} catch {
|
|
413
|
+
body = null;
|
|
414
|
+
}
|
|
415
|
+
if (body === null) return new Response("Invalid request body", { status: 400 });
|
|
416
|
+
const action = body.action;
|
|
417
|
+
const args = typeof body.args === "object" && body.args !== null ? { ...body.args } : {};
|
|
418
|
+
if (args.refreshToken === null) args.refreshToken = void 0;
|
|
419
|
+
const actionDispatch = action === "auth:signIn" ? { action: "sessionStart" } : action === "auth:signOut" ? { action: "sessionStop" } : null;
|
|
420
|
+
if (actionDispatch === null) return new Response("Invalid action", { status: 400 });
|
|
421
|
+
const host = request.headers.get("host") ?? new URL(request.url).host;
|
|
422
|
+
const currentCookies = parseAuthCookies(request.headers.get("cookie"), host, cookieNamespace);
|
|
423
|
+
if (actionDispatch.action === "sessionStart") {
|
|
424
|
+
let refreshResponse = null;
|
|
425
|
+
if (args.refreshToken === void 0) refreshResponse = null;
|
|
426
|
+
else if (currentCookies.refreshToken === null) {
|
|
427
|
+
const currentToken = currentCookies.token;
|
|
428
|
+
const decodedToken = currentToken === null ? null : decodeToken(currentToken);
|
|
429
|
+
if (currentToken !== null && decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now()) refreshResponse = jsonResponse({ tokens: {
|
|
430
|
+
token: currentToken,
|
|
431
|
+
refreshToken: "dummy"
|
|
432
|
+
} });
|
|
433
|
+
else refreshResponse = jsonResponse({ tokens: null });
|
|
434
|
+
} else {
|
|
435
|
+
args.refreshToken = currentCookies.refreshToken ?? void 0;
|
|
436
|
+
refreshResponse = null;
|
|
437
|
+
}
|
|
438
|
+
if (refreshResponse !== null) return refreshResponse;
|
|
439
|
+
const { client, cookies: effectiveCookies } = await hydrateProxySignInClient(currentCookies, args);
|
|
440
|
+
try {
|
|
441
|
+
return toSignInProxyResponse(await runSignIn(client, args), args, effectiveCookies, host);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return appendCookieHeaders(jsonResponse(getProxyErrorBody(error), 400), serializeAuthCookies({
|
|
444
|
+
token: effectiveCookies.token,
|
|
445
|
+
refreshToken: effectiveCookies.refreshToken,
|
|
446
|
+
verifier: null
|
|
447
|
+
}, host, cookieConfig, cookieNamespace));
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
try {
|
|
451
|
+
await runSignOut(currentCookies.token);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
log("ERROR", "[convex-auth/server] proxy sign-out failed", error);
|
|
454
|
+
if (currentCookies.refreshToken !== null) try {
|
|
455
|
+
const refreshedTokens = extractSignedInTokens(await runSignIn(createClient(), { refreshToken: currentCookies.refreshToken }), "sign-out fallback refresh");
|
|
456
|
+
if (refreshedTokens !== null) await runSignOut(refreshedTokens.token);
|
|
457
|
+
} catch (fallbackError) {
|
|
458
|
+
log("ERROR", "[convex-auth/server] proxy sign-out fallback failed", fallbackError);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return appendCookieHeaders(jsonResponse(null), serializeAuthCookies({
|
|
462
|
+
token: null,
|
|
463
|
+
refreshToken: null,
|
|
464
|
+
verifier: null
|
|
465
|
+
}, host, cookieConfig, cookieNamespace));
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
async refresh(request) {
|
|
469
|
+
const createClient = () => new ConvexHttpClient(convexUrl);
|
|
470
|
+
const logVerbose = (message) => {
|
|
471
|
+
if (verbose) log("DEBUG", `${(/* @__PURE__ */ new Date()).toISOString()} [convex-auth/server] ${message}`);
|
|
472
|
+
};
|
|
473
|
+
const refreshWithToken = async (refreshToken$1) => {
|
|
474
|
+
try {
|
|
475
|
+
const tokens$1 = extractSignedInTokens(await createClient().action(signInActionRef, { refreshToken: refreshToken$1 }), "token refresh");
|
|
476
|
+
logVerbose(`Refreshed tokens, null=${tokens$1 === null}`);
|
|
477
|
+
return tokens$1;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
log("ERROR", "[convex-auth/server] refresh-token exchange failed", error);
|
|
480
|
+
if (getConvexErrorCode(error) === "INVALID_REFRESH_TOKEN") {
|
|
481
|
+
logVerbose("Refresh token rejected, clearing auth cookies");
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
logVerbose("Token refresh failed transiently, keeping current cookies");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const host = request.headers.get("host") ?? new URL(request.url).host;
|
|
489
|
+
const currentCookies = parseAuthCookies(request.headers.get("cookie"), host, cookieNamespace);
|
|
490
|
+
const currentToken = currentCookies.token;
|
|
491
|
+
const originHeader = request.headers.get("origin");
|
|
492
|
+
const forwardedProtoHeader = request.headers.get("x-forwarded-proto");
|
|
493
|
+
const protocol = forwardedProtoHeader !== null ? (() => {
|
|
494
|
+
const forwardedProto = forwardedProtoHeader.split(",")[0]?.trim();
|
|
495
|
+
if (forwardedProto !== void 0 && forwardedProto.length > 0) return forwardedProto.endsWith(":") ? forwardedProto : `${forwardedProto}:`;
|
|
496
|
+
return new URL(request.url).protocol;
|
|
497
|
+
})() : new URL(request.url).protocol;
|
|
498
|
+
const requestHost = request.headers.get("host") ?? new URL(request.url).host;
|
|
499
|
+
const hostCandidate = `${protocol}//${requestHost}`;
|
|
500
|
+
const normalizedHost = canParseUrl(hostCandidate) ? new URL(hostCandidate).host : requestHost;
|
|
501
|
+
const originUrl = originHeader !== null && canParseUrl(originHeader) ? new URL(originHeader) : null;
|
|
502
|
+
if (originHeader !== null && (originUrl === null || originUrl.host !== normalizedHost || originUrl.protocol !== protocol)) return {
|
|
503
|
+
redirect: false,
|
|
504
|
+
cookies: [],
|
|
505
|
+
token: null
|
|
506
|
+
};
|
|
507
|
+
const requestUrl = new URL(request.url);
|
|
508
|
+
const code = requestUrl.searchParams.get("code");
|
|
509
|
+
const shouldHandleCodeOption = options.shouldHandleCode;
|
|
510
|
+
let shouldHandleCode;
|
|
511
|
+
if (shouldHandleCodeOption === void 0) shouldHandleCode = true;
|
|
512
|
+
else if (typeof shouldHandleCodeOption === "function") {
|
|
513
|
+
const result = shouldHandleCodeOption(request);
|
|
514
|
+
shouldHandleCode = typeof result === "boolean" ? result : await result;
|
|
515
|
+
} else shouldHandleCode = shouldHandleCodeOption;
|
|
516
|
+
let codeExchangeResult = null;
|
|
517
|
+
if (code !== null && request.method === "GET" && request.headers.get("accept")?.includes("text/html") && shouldHandleCode) {
|
|
518
|
+
const redirectUrl = new URL(requestUrl.toString());
|
|
519
|
+
try {
|
|
520
|
+
const tokens$1 = extractSignedInTokens(await createClient().action(signInActionRef, {
|
|
521
|
+
params: { code },
|
|
522
|
+
verifier: currentCookies.verifier ?? void 0
|
|
523
|
+
}), "code exchange");
|
|
524
|
+
redirectUrl.searchParams.delete("code");
|
|
525
|
+
const cookies = structuredAuthCookies({
|
|
526
|
+
token: tokens$1?.token ?? null,
|
|
527
|
+
refreshToken: tokens$1?.refreshToken ?? null,
|
|
528
|
+
verifier: null
|
|
529
|
+
}, host, cookieConfig, cookieNamespace);
|
|
530
|
+
codeExchangeResult = {
|
|
531
|
+
redirect: true,
|
|
532
|
+
response: buildRedirectResponse(redirectUrl.toString(), cookies)
|
|
533
|
+
};
|
|
534
|
+
} catch (error) {
|
|
535
|
+
log("ERROR", "[convex-auth/server] code exchange failed", error);
|
|
536
|
+
if (![
|
|
537
|
+
"OAUTH_INVALID_STATE",
|
|
538
|
+
"OAUTH_PROVIDER_ERROR",
|
|
539
|
+
"OAUTH_MISSING_ID_TOKEN",
|
|
540
|
+
"OAUTH_INVALID_PROFILE",
|
|
541
|
+
"OAUTH_MISSING_VERIFIER",
|
|
542
|
+
"INVALID_VERIFIER",
|
|
543
|
+
"INVALID_VERIFICATION_CODE"
|
|
544
|
+
].includes(getConvexErrorCode(error) ?? "")) codeExchangeResult = {
|
|
545
|
+
redirect: false,
|
|
546
|
+
cookies: [],
|
|
547
|
+
token: currentCookies.token
|
|
548
|
+
};
|
|
549
|
+
else {
|
|
555
550
|
redirectUrl.searchParams.delete("code");
|
|
556
551
|
const cookies = structuredAuthCookies({
|
|
557
552
|
token: currentCookies.token,
|
|
558
553
|
refreshToken: currentCookies.refreshToken,
|
|
559
554
|
verifier: null
|
|
560
555
|
}, host, cookieConfig, cookieNamespace);
|
|
561
|
-
|
|
556
|
+
codeExchangeResult = {
|
|
562
557
|
redirect: true,
|
|
563
558
|
response: buildRedirectResponse(redirectUrl.toString(), cookies)
|
|
564
559
|
};
|
|
565
|
-
}
|
|
566
|
-
}), Match.exhaustive);
|
|
567
|
-
if (codeExchangeResult !== null) return codeExchangeResult;
|
|
568
|
-
const { token, refreshToken } = currentCookies;
|
|
569
|
-
if (refreshToken !== null && (refreshToken.trim().length === 0 || refreshToken === "dummy")) {
|
|
570
|
-
yield* logVerbose("Refresh token cookie malformed, clearing auth cookies");
|
|
571
|
-
return {
|
|
572
|
-
redirect: false,
|
|
573
|
-
cookies: structuredAuthCookies({
|
|
574
|
-
token: null,
|
|
575
|
-
refreshToken: null,
|
|
576
|
-
verifier: null
|
|
577
|
-
}, host, cookieConfig, cookieNamespace),
|
|
578
|
-
token: null
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
const decodedToken = token === null ? null : yield* decodeToken(token);
|
|
582
|
-
if (decodedToken?.iss !== void 0 && !acceptedIssuers.has(normalizeIssuer(decodedToken.iss))) {
|
|
583
|
-
yield* logVerbose("Access token issuer mismatch, clearing auth cookies");
|
|
584
|
-
return {
|
|
585
|
-
redirect: false,
|
|
586
|
-
cookies: structuredAuthCookies({
|
|
587
|
-
token: null,
|
|
588
|
-
refreshToken: null,
|
|
589
|
-
verifier: null
|
|
590
|
-
}, host, cookieConfig, cookieNamespace),
|
|
591
|
-
token: null
|
|
592
|
-
};
|
|
560
|
+
}
|
|
593
561
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
} : {
|
|
601
|
-
kind: "both",
|
|
602
|
-
token,
|
|
603
|
-
refreshToken
|
|
604
|
-
}).pipe(Match.when({ kind: "none" }, () => logVerbose("No auth cookies found, skipping refresh").pipe(Effect.as(void 0))), Match.when({ kind: "refreshOnly" }, ({ refreshToken: refreshToken$1 }) => logVerbose("Access token cookie missing, attempting refresh-token recovery").pipe(Effect.andThen(() => refreshWithToken(refreshToken$1)))), Match.when({ kind: "accessOnly" }, () => decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now() ? logVerbose("Refresh token cookie missing but access token still valid").pipe(Effect.as(void 0)) : logVerbose("Refresh token cookie missing and access token invalid, clearing").pipe(Effect.as(null))), Match.when({ kind: "both" }, ({ refreshToken: refreshToken$1 }) => Match.value(decodedToken?.exp === void 0 || decodedToken.iat === void 0 ? { kind: "undecodable" } : {
|
|
605
|
-
kind: "decoded",
|
|
606
|
-
decodedToken
|
|
607
|
-
}).pipe(Match.when({ kind: "undecodable" }, () => logVerbose("Failed to decode access token, attempting refresh-token recovery").pipe(Effect.andThen(() => refreshWithToken(refreshToken$1)))), Match.when({ kind: "decoded" }, ({ decodedToken: decodedToken$1 }) => {
|
|
608
|
-
const totalTokenLifetimeMs = decodedToken$1.exp * 1e3 - decodedToken$1.iat * 1e3;
|
|
609
|
-
const minimumExpiration = Date.now() + Math.min(REQUIRED_TOKEN_LIFETIME_MS, Math.max(MINIMUM_REQUIRED_TOKEN_LIFETIME_MS, totalTokenLifetimeMs / 10));
|
|
610
|
-
return decodedToken$1.exp * 1e3 > minimumExpiration ? logVerbose("Token valid long enough, skipping refresh").pipe(Effect.as(void 0)) : refreshWithToken(refreshToken$1);
|
|
611
|
-
}), Match.exhaustive)), Match.exhaustive);
|
|
612
|
-
if (tokens === void 0) return {
|
|
562
|
+
}
|
|
563
|
+
if (codeExchangeResult !== null) return codeExchangeResult;
|
|
564
|
+
const { token, refreshToken } = currentCookies;
|
|
565
|
+
if (refreshToken !== null && (refreshToken.trim().length === 0 || refreshToken === "dummy")) {
|
|
566
|
+
logVerbose("Refresh token cookie malformed, clearing auth cookies");
|
|
567
|
+
return {
|
|
613
568
|
redirect: false,
|
|
614
|
-
cookies:
|
|
615
|
-
|
|
569
|
+
cookies: structuredAuthCookies({
|
|
570
|
+
token: null,
|
|
571
|
+
refreshToken: null,
|
|
572
|
+
verifier: null
|
|
573
|
+
}, host, cookieConfig, cookieNamespace),
|
|
574
|
+
token: null
|
|
616
575
|
};
|
|
576
|
+
}
|
|
577
|
+
const decodedToken = token === null ? null : decodeToken(token);
|
|
578
|
+
if (decodedToken?.iss !== void 0 && !acceptedIssuers.has(normalizeIssuer(decodedToken.iss))) {
|
|
579
|
+
logVerbose("Access token issuer mismatch, clearing auth cookies");
|
|
617
580
|
return {
|
|
618
581
|
redirect: false,
|
|
619
582
|
cookies: structuredAuthCookies({
|
|
620
|
-
token:
|
|
621
|
-
refreshToken:
|
|
583
|
+
token: null,
|
|
584
|
+
refreshToken: null,
|
|
622
585
|
verifier: null
|
|
623
586
|
}, host, cookieConfig, cookieNamespace),
|
|
624
|
-
token:
|
|
587
|
+
token: null
|
|
625
588
|
};
|
|
626
|
-
}
|
|
589
|
+
}
|
|
590
|
+
let tokens;
|
|
591
|
+
if (token === null && refreshToken === null) {
|
|
592
|
+
logVerbose("No auth cookies found, skipping refresh");
|
|
593
|
+
tokens = void 0;
|
|
594
|
+
} else if (token === null && refreshToken !== null) {
|
|
595
|
+
logVerbose("Access token cookie missing, attempting refresh-token recovery");
|
|
596
|
+
tokens = await refreshWithToken(refreshToken);
|
|
597
|
+
} else if (token !== null && refreshToken === null) if (decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now()) {
|
|
598
|
+
logVerbose("Refresh token cookie missing but access token still valid");
|
|
599
|
+
tokens = void 0;
|
|
600
|
+
} else {
|
|
601
|
+
logVerbose("Refresh token cookie missing and access token invalid, clearing");
|
|
602
|
+
tokens = null;
|
|
603
|
+
}
|
|
604
|
+
else if (decodedToken?.exp === void 0 || decodedToken.iat === void 0) {
|
|
605
|
+
logVerbose("Failed to decode access token, attempting refresh-token recovery");
|
|
606
|
+
tokens = await refreshWithToken(refreshToken);
|
|
607
|
+
} else {
|
|
608
|
+
const totalTokenLifetimeMs = decodedToken.exp * 1e3 - decodedToken.iat * 1e3;
|
|
609
|
+
const minimumExpiration = Date.now() + Math.min(REQUIRED_TOKEN_LIFETIME_MS, Math.max(MINIMUM_REQUIRED_TOKEN_LIFETIME_MS, totalTokenLifetimeMs / 10));
|
|
610
|
+
if (decodedToken.exp * 1e3 > minimumExpiration) {
|
|
611
|
+
logVerbose("Token valid long enough, skipping refresh");
|
|
612
|
+
tokens = void 0;
|
|
613
|
+
} else tokens = await refreshWithToken(refreshToken);
|
|
614
|
+
}
|
|
615
|
+
if (tokens === void 0) return {
|
|
616
|
+
redirect: false,
|
|
617
|
+
cookies: [],
|
|
618
|
+
token: currentToken
|
|
619
|
+
};
|
|
620
|
+
return {
|
|
621
|
+
redirect: false,
|
|
622
|
+
cookies: structuredAuthCookies({
|
|
623
|
+
token: tokens?.token ?? null,
|
|
624
|
+
refreshToken: tokens?.refreshToken ?? null,
|
|
625
|
+
verifier: null
|
|
626
|
+
}, host, cookieConfig, cookieNamespace),
|
|
627
|
+
token: tokens?.token ?? null
|
|
628
|
+
};
|
|
627
629
|
}
|
|
628
630
|
};
|
|
629
631
|
}
|