@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.
Files changed (88) hide show
  1. package/README.md +3 -5
  2. package/dist/bin.js +6488 -1571
  3. package/dist/browser/index.js +10 -7
  4. package/dist/browser/locks.js +3 -5
  5. package/dist/browser/navigation.js +7 -10
  6. package/dist/browser/runtime.js +35 -33
  7. package/dist/client/core/types.js +17 -0
  8. package/dist/client/factors/device.js +26 -19
  9. package/dist/client/index.js +151 -163
  10. package/dist/client/runtime/proxy.js +6 -6
  11. package/dist/client/services/adapters.js +3 -7
  12. package/dist/client/services/http.js +2 -5
  13. package/dist/client/services/resolve.js +5 -11
  14. package/dist/client/services/runtime.js +2 -5
  15. package/dist/component/_generated/component.d.ts +46 -0
  16. package/dist/component/index.d.ts +3 -3
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/public/identity/sessions.js +38 -1
  19. package/dist/component/public/identity/tokens.js +81 -3
  20. package/dist/component/public/identity/verifiers.js +9 -3
  21. package/dist/component/public.js +3 -3
  22. package/dist/component/schema.d.ts +320 -320
  23. package/dist/core/index.d.ts +380 -0
  24. package/dist/core/index.js +83 -0
  25. package/dist/otel.d.ts +13 -17
  26. package/dist/otel.js +39 -49
  27. package/dist/providers/email.d.ts +2 -2
  28. package/dist/providers/password.js +8 -16
  29. package/dist/providers/phone.js +2 -9
  30. package/dist/server/auth-context.d.ts +204 -0
  31. package/dist/server/auth-context.js +76 -0
  32. package/dist/server/auth.d.ts +25 -187
  33. package/dist/server/auth.js +5 -96
  34. package/dist/server/componentContext.d.ts +12 -0
  35. package/dist/server/componentContext.js +1 -0
  36. package/dist/server/config.js +1 -12
  37. package/dist/server/constants.js +6 -0
  38. package/dist/server/contract.d.ts +1 -1
  39. package/dist/server/core.js +5 -14
  40. package/dist/server/crypto.js +26 -18
  41. package/dist/server/db.js +6 -1
  42. package/dist/server/device.js +88 -78
  43. package/dist/server/http.d.ts +4 -3
  44. package/dist/server/http.js +74 -86
  45. package/dist/server/index.d.ts +2 -1
  46. package/dist/server/limits.js +22 -15
  47. package/dist/server/mounts.d.ts +103 -103
  48. package/dist/server/mutations/account.js +6 -4
  49. package/dist/server/mutations/invalidate.js +3 -6
  50. package/dist/server/mutations/oauth.js +86 -88
  51. package/dist/server/mutations/refresh.js +45 -87
  52. package/dist/server/mutations/register.js +19 -19
  53. package/dist/server/mutations/retrieve.js +17 -15
  54. package/dist/server/mutations/signature.js +9 -13
  55. package/dist/server/mutations/signin.js +7 -3
  56. package/dist/server/mutations/signout.js +10 -15
  57. package/dist/server/mutations/store.js +22 -12
  58. package/dist/server/mutations/verifier.js +11 -6
  59. package/dist/server/mutations/verify.js +55 -46
  60. package/dist/server/oauth/runtime.js +27 -25
  61. package/dist/server/passkey.js +299 -250
  62. package/dist/server/prefetch.js +283 -281
  63. package/dist/server/refresh.js +7 -60
  64. package/dist/server/runtime.d.ts +82 -206
  65. package/dist/server/runtime.js +63 -56
  66. package/dist/server/services/config.js +5 -3
  67. package/dist/server/services/logger.js +2 -4
  68. package/dist/server/services/providers.js +2 -4
  69. package/dist/server/services/refresh.js +2 -4
  70. package/dist/server/services/resolve.js +15 -14
  71. package/dist/server/services/signin.js +2 -4
  72. package/dist/server/sessions.js +32 -33
  73. package/dist/server/signin.js +177 -142
  74. package/dist/server/sso/domain.d.ts +20 -68
  75. package/dist/server/sso/domain.js +444 -413
  76. package/dist/server/sso/http.js +53 -59
  77. package/dist/server/sso/oidc.js +94 -80
  78. package/dist/server/tokens.js +13 -3
  79. package/dist/server/totp.js +153 -116
  80. package/dist/server/types.d.ts +2 -2
  81. package/dist/server/users.js +18 -23
  82. package/dist/server/utils/cache.js +51 -0
  83. package/dist/server/utils/dispatch.js +36 -0
  84. package/dist/server/utils/retry.js +24 -0
  85. package/dist/server/utils/span.js +32 -0
  86. package/dist/shared/errors.js +9 -3
  87. package/dist/shared/log.js +20 -22
  88. package/package.json +41 -33
@@ -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 = 6e4;
160
- const MINIMUM_REQUIRED_TOKEN_LIFETIME_MS = 1e4;
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
- return Effect.try({
170
- try: () => jwtDecode(token),
171
- catch: () => null
172
- }).pipe(Effect.catch(() => Effect.succeed(null)));
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
- return Match.value(result).pipe(Match.when({ kind: "signedIn" }, ({ tokens }) => Effect.succeed(tokens)), Match.orElse(() => Effect.fail(/* @__PURE__ */ new Error(`Invalid \`auth:signIn\` result for ${context}`))));
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
- return runBoundary(Effect.gen(function* () {
306
- const token = parseAuthCookies(request.headers.get("cookie"), request.headers.get("host") ?? new URL(request.url).host, cookieNamespace).token;
307
- if (token === null) return false;
308
- const decodedToken = yield* decodeToken(token);
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) => Effect.tryPromise({
319
- try: () => client.action(signInActionRef, args),
320
- catch: (error) => error
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 decodedToken = currentToken === null ? null : yield* decodeToken(currentToken);
335
- if (currentToken !== null && decodedToken?.exp !== void 0 && decodedToken.iss !== void 0 && acceptedIssuers.has(normalizeIssuer(decodedToken.iss)) && decodedToken.exp * 1e3 > Date.now()) {
336
- yield* Effect.sync(() => {
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
- const refreshedTokens = yield* runSignIn(createClient(), { refreshToken: currentCookies.refreshToken }).pipe(Effect.flatMap((result) => extractSignedInTokens(result, "proxy sign-in auth hydration")), Effect.catch(() => Effect.succeed(null)));
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
- yield* Effect.sync(() => {
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) => Match.value(result).pipe(Match.when({ kind: "redirect" }, (redirectResult) => Effect.sync(() => appendCookieHeaders(jsonResponse({
366
- kind: "redirect",
367
- redirect: redirectResult.redirect,
368
- verifier: redirectResult.verifier
369
- }), serializeAuthCookies({
370
- ...currentCookies,
371
- verifier: redirectResult.verifier
372
- }, host, cookieConfig, cookieNamespace)))), Match.when({ kind: "signedIn" }, (signedInResult) => Effect.sync(() => {
373
- const nextCookies = signedInResult.tokens === null ? {
374
- token: currentCookies.token,
375
- refreshToken: currentCookies.refreshToken,
376
- verifier: null
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
- }, host, cookieConfig, cookieNamespace)))));
454
- })), Match.when({ action: "sessionStop" }, () => Effect.gen(function* () {
455
- yield* runSignOut(currentCookies.token).pipe(Effect.catch((error) => Effect.gen(function* () {
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
- }, host, cookieConfig, cookieNamespace));
470
- })), Match.exhaustive);
471
- }));
472
- },
473
- async refresh(request) {
474
- const createClient = () => new ConvexHttpClient(convexUrl);
475
- const logVerbose = (message) => verbose ? Effect.sync(() => {
476
- log("DEBUG", `${(/* @__PURE__ */ new Date()).toISOString()} [convex-auth/server] ${message}`);
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
- yield* logVerbose("Token refresh failed transiently, keeping current cookies");
490
- })));
491
- return runBoundary(Effect.gen(function* () {
492
- const host = request.headers.get("host") ?? new URL(request.url).host;
493
- const currentCookies = parseAuthCookies(request.headers.get("cookie"), host, cookieNamespace);
494
- const currentToken = currentCookies.token;
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 normalizedHost = canParseUrl(hostCandidate) ? new URL(hostCandidate).host : requestHost;
505
- const originUrl = originHeader !== null && canParseUrl(originHeader) ? new URL(originHeader) : null;
506
- if (originHeader !== null && (originUrl === null || originUrl.host !== normalizedHost || originUrl.protocol !== protocol)) return {
507
- redirect: false,
508
- cookies: [],
509
- token: null
510
- };
511
- const requestUrl = new URL(request.url);
512
- const code = requestUrl.searchParams.get("code");
513
- const shouldHandleCodeOption = options.shouldHandleCode;
514
- const shouldHandleCode = shouldHandleCodeOption === void 0 ? true : typeof shouldHandleCodeOption === "function" ? yield* Effect.sync(() => shouldHandleCodeOption(request)).pipe(Effect.flatMap((result) => typeof result === "boolean" ? Effect.succeed(result) : Effect.promise(() => result))) : shouldHandleCodeOption;
515
- const codeExchangeResult = yield* Match.value(code !== null && request.method === "GET" && request.headers.get("accept")?.includes("text/html") && shouldHandleCode ? {
516
- kind: "exchange",
517
- code
518
- } : { kind: "skip" }).pipe(Match.when({ kind: "skip" }, () => Effect.succeed(null)), Match.when({ kind: "exchange" }, ({ code: verificationCode }) => {
519
- const redirectUrl = new URL(requestUrl.toString());
520
- return Effect.gen(function* () {
521
- const result = yield* Effect.tryPromise({
522
- try: () => createClient().action(signInActionRef, {
523
- params: { code: verificationCode },
524
- verifier: currentCookies.verifier ?? void 0
525
- }),
526
- catch: (error) => error
527
- }).pipe(Effect.flatMap((value) => extractSignedInTokens(value, "code exchange")));
528
- redirectUrl.searchParams.delete("code");
529
- const cookies = structuredAuthCookies({
530
- token: result?.token ?? null,
531
- refreshToken: result?.refreshToken ?? null,
532
- verifier: null
533
- }, host, cookieConfig, cookieNamespace);
534
- return {
535
- redirect: true,
536
- response: buildRedirectResponse(redirectUrl.toString(), cookies)
537
- };
538
- }).pipe(Effect.catch((error) => Effect.gen(function* () {
539
- yield* Effect.sync(() => {
540
- log("ERROR", "[convex-auth/server] code exchange failed", error);
541
- });
542
- if (![
543
- "OAUTH_INVALID_STATE",
544
- "OAUTH_PROVIDER_ERROR",
545
- "OAUTH_MISSING_ID_TOKEN",
546
- "OAUTH_INVALID_PROFILE",
547
- "OAUTH_MISSING_VERIFIER",
548
- "INVALID_VERIFIER",
549
- "INVALID_VERIFICATION_CODE"
550
- ].includes(getConvexErrorCode(error) ?? "")) return {
551
- redirect: false,
552
- cookies: [],
553
- token: currentCookies.token
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
- return {
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
- const tokens = yield* Match.value(token === null ? refreshToken === null ? { kind: "none" } : {
595
- kind: "refreshOnly",
596
- refreshToken
597
- } : refreshToken === null ? {
598
- kind: "accessOnly",
599
- token
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
- token: currentToken
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: tokens?.token ?? null,
621
- refreshToken: tokens?.refreshToken ?? null,
583
+ token: null,
584
+ refreshToken: null,
622
585
  verifier: null
623
586
  }, host, cookieConfig, cookieNamespace),
624
- token: tokens?.token ?? null
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
  }