@robelest/convex-auth 0.0.4-preview.30 → 0.0.4-preview.31

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 (105) hide show
  1. package/dist/bin.js +125 -36
  2. package/dist/browser/index.d.ts +3 -13
  3. package/dist/browser/index.js +47 -12
  4. package/dist/browser/navigation.js +1 -1
  5. package/dist/browser/passkey.js +7 -7
  6. package/dist/browser/runtime.js +13 -15
  7. package/dist/client/core/types.d.ts +179 -63
  8. package/dist/client/core/types.js +6 -0
  9. package/dist/client/factors/totp.js +1 -1
  10. package/dist/client/index.d.ts +5 -4
  11. package/dist/client/index.js +115 -56
  12. package/dist/client/runtime/mutex.js +3 -2
  13. package/dist/component/_generated/component.d.ts +40 -0
  14. package/dist/component/convex.config.d.ts +2 -2
  15. package/dist/component/http.js +9 -0
  16. package/dist/component/index.d.ts +1 -1
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/model.js +2 -1
  19. package/dist/component/modules.js +1 -0
  20. package/dist/component/public/factors/passkeys.js +31 -1
  21. package/dist/component/public/identity/codes.js +1 -1
  22. package/dist/component/public/identity/tokens.js +2 -1
  23. package/dist/component/public/identity/verifiers.js +15 -5
  24. package/dist/component/public.js +2 -2
  25. package/dist/component/schema.d.ts +292 -290
  26. package/dist/component/schema.js +2 -1
  27. package/dist/core/index.d.ts +8 -3
  28. package/dist/core/index.js +7 -2
  29. package/dist/expo/index.d.ts +21 -0
  30. package/dist/expo/index.js +148 -0
  31. package/dist/expo/passkey.js +174 -0
  32. package/dist/providers/apple.d.ts +1 -1
  33. package/dist/providers/apple.js +6 -8
  34. package/dist/providers/custom.d.ts +1 -1
  35. package/dist/providers/custom.js +4 -7
  36. package/dist/providers/github.d.ts +1 -1
  37. package/dist/providers/github.js +5 -8
  38. package/dist/providers/google.d.ts +1 -1
  39. package/dist/providers/google.js +5 -8
  40. package/dist/providers/microsoft.d.ts +1 -1
  41. package/dist/providers/microsoft.js +5 -9
  42. package/dist/providers/password.d.ts +18 -37
  43. package/dist/providers/password.js +170 -115
  44. package/dist/providers/redirect.d.ts +1 -0
  45. package/dist/providers/redirect.js +20 -0
  46. package/dist/server/auth.d.ts +6 -7
  47. package/dist/server/auth.js +3 -2
  48. package/dist/server/{ctxCache.js → cache/context.js} +2 -2
  49. package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
  50. package/dist/server/context.js +3 -10
  51. package/dist/server/contract.d.ts +2 -87
  52. package/dist/server/contract.js +1 -1
  53. package/dist/server/cookies.js +25 -1
  54. package/dist/server/core.js +1 -1
  55. package/dist/server/errors.js +24 -1
  56. package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
  57. package/dist/server/{auth-context.js → facade.js} +11 -12
  58. package/dist/server/http.d.ts +7 -7
  59. package/dist/server/http.js +36 -7
  60. package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
  61. package/dist/server/index.d.ts +5 -3
  62. package/dist/server/index.js +3 -2
  63. package/dist/server/mounts.d.ts +175 -22
  64. package/dist/server/mutations/code.js +7 -1
  65. package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
  66. package/dist/server/mutations/index.js +1 -1
  67. package/dist/server/mutations/invalidate.js +11 -1
  68. package/dist/server/mutations/oauth.js +25 -27
  69. package/dist/server/mutations/signin.js +6 -0
  70. package/dist/server/mutations/signout.js +5 -0
  71. package/dist/server/mutations/store.js +1 -1
  72. package/dist/server/oauth/factory.js +11 -3
  73. package/dist/server/passkey.js +126 -110
  74. package/dist/server/prefetch.js +8 -1
  75. package/dist/server/redirects.js +11 -3
  76. package/dist/server/refresh.js +6 -1
  77. package/dist/server/runtime.d.ts +58 -36
  78. package/dist/server/runtime.js +333 -36
  79. package/dist/server/services/group.js +4 -0
  80. package/dist/server/sessions.js +1 -0
  81. package/dist/server/signin.js +8 -6
  82. package/dist/server/sso/domain.d.ts +159 -16
  83. package/dist/server/sso/domain.js +1 -1
  84. package/dist/server/sso/http.js +144 -60
  85. package/dist/server/sso/oidc.js +28 -12
  86. package/dist/server/sso/policy.js +30 -14
  87. package/dist/server/sso/provision.js +1 -1
  88. package/dist/server/sso/saml.js +18 -9
  89. package/dist/server/sso/scim.js +12 -4
  90. package/dist/server/sso/shared.js +5 -5
  91. package/dist/server/telemetry.js +3 -0
  92. package/dist/server/tokens.js +10 -2
  93. package/dist/server/totp.js +127 -100
  94. package/dist/server/types.d.ts +224 -151
  95. package/dist/server/url.js +1 -1
  96. package/dist/server/users.js +93 -53
  97. package/dist/server/wellknown.d.ts +130 -0
  98. package/dist/server/wellknown.js +195 -0
  99. package/dist/shared/errors.js +0 -1
  100. package/package.json +36 -4
  101. package/dist/server/oauth/index.js +0 -12
  102. package/dist/server/utils/dispatch.js +0 -36
  103. package/dist/shared/authResults.d.ts +0 -16
  104. /package/dist/server/{componentContext.js → component/context.js} +0 -0
  105. /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { LOG_LEVELS } from "../shared/log.js";
2
2
  import { generateRandomString, sha256 } from "./random.js";
3
3
  import { createCoreDomains } from "./core.js";
4
- import { requireEnv } from "./env.js";
4
+ import { envOptionalString, readConfigSync, requireEnv } from "./env.js";
5
5
  import { log, logError } from "./log.js";
6
6
  import { callModifyAccount } from "./mutations/account.js";
7
7
  import { callInvalidateSessions } from "./mutations/invalidate.js";
@@ -14,8 +14,10 @@ import { callVerifierSignature } from "./mutations/signature.js";
14
14
  import { callSignOut } from "./mutations/signout.js";
15
15
  import { storeArgs, storeImpl } from "./mutations/store.js";
16
16
  import { siteUrlsFromEnv } from "./url.js";
17
- import { redirectToParamCookie, useRedirectToParam } from "./cookies.js";
18
- import { addAuthRoutes, addOpenIdRoutes, convertErrorsToResponse, createHttpAction, createHttpContext, createHttpRoute, getCookies } from "./http.js";
17
+ import { decodeOAuthState, encodeOAuthState } from "./cookies.js";
18
+ import { FlowSignal } from "./errors.js";
19
+ import { addAuthRoutes, addOpenIdRoutes, addWebAuthnRoute, convertErrorsToResponse, createHttpAction, createHttpContext, createHttpRoute, getCookies } from "./http.js";
20
+ import { generateWebAuthnConfig } from "./wellknown.js";
19
21
  import { createOAuthAuthorizationURL, handleOAuthCallback } from "./oauth/runtime.js";
20
22
  import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
21
23
  import { encryptSecret } from "./secret.js";
@@ -24,11 +26,61 @@ import { resolveServerServices } from "./services/resolve.js";
24
26
  import { createGroupConnectionDomain } from "./sso/domain.js";
25
27
  import { addGroupHttpRuntime } from "./sso/http.js";
26
28
  import { ConvexError, v } from "convex/values";
27
- import { actionGeneric, internalMutationGeneric } from "convex/server";
29
+ import { actionGeneric, httpRouter, internalMutationGeneric } from "convex/server";
28
30
  import { serialize } from "cookie";
29
31
 
30
32
  //#region src/server/runtime.ts
31
33
  const GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND = "oidc_client_secret";
34
+ const vHeaderPairs = v.array(v.array(v.string()));
35
+ function formDataEntries(formData) {
36
+ return formData;
37
+ }
38
+ const vHttpDelegateResponse = v.object({
39
+ status: v.number(),
40
+ statusText: v.string(),
41
+ headers: vHeaderPairs,
42
+ body: v.string()
43
+ });
44
+ function normalizeRoutePrefix(prefix) {
45
+ if (prefix === void 0 || prefix === "" || prefix === "/") return "";
46
+ return (prefix.startsWith("/") ? prefix : `/${prefix}`).replace(/\/$/, "");
47
+ }
48
+ function appendRoutePrefix(siteUrl, prefix) {
49
+ return `${siteUrl.replace(/\/$/, "")}${prefix}`;
50
+ }
51
+ function authSiteUrlFromEnv() {
52
+ return appendRoutePrefix(readConfigSync(envOptionalString("CONVEX_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL"), normalizeRoutePrefix(readConfigSync(envOptionalString("CONVEX_AUTH_HTTP_PREFIX")) ?? "/auth"));
53
+ }
54
+ function createInMemoryHttpRouter() {
55
+ const routes = [];
56
+ return {
57
+ route: (spec) => {
58
+ routes.push(spec);
59
+ },
60
+ lookup(pathname, method) {
61
+ const exact = routes.find((route) => route.method === method && route.path === pathname);
62
+ if (exact) return exact;
63
+ return routes.filter((route) => route.method === method && route.pathPrefix !== void 0 && pathname.startsWith(route.pathPrefix)).sort((a, b) => (b.pathPrefix?.length ?? 0) - (a.pathPrefix?.length ?? 0))[0] ?? null;
64
+ }
65
+ };
66
+ }
67
+ async function invokeHttpHandler(handler, ctx, request) {
68
+ const maybeHandler = handler;
69
+ if (typeof maybeHandler._handler === "function") return await maybeHandler._handler(ctx, request);
70
+ throw new Error("Invalid HTTP action handler.");
71
+ }
72
+ async function serializeHttpResponse(response) {
73
+ const headers = [];
74
+ response.headers.forEach((value, name) => headers.push([name, value]));
75
+ const setCookies = response.headers.getSetCookie?.();
76
+ if (setCookies !== void 0) for (const cookie of setCookies) headers.push(["set-cookie", cookie]);
77
+ return {
78
+ status: response.status,
79
+ statusText: response.statusText,
80
+ headers,
81
+ body: await response.text()
82
+ };
83
+ }
32
84
  const convexError = (data) => new ConvexError(data);
33
85
  /**
34
86
  * Configure the Convex Auth library. Returns an object with
@@ -42,7 +94,10 @@ const convexError = (data) => new ConvexError(data);
42
94
  * export const auth = createAuth(components.auth, {
43
95
  * providers: [],
44
96
  * });
45
- * export const { signIn, signOut, store } = auth;
97
+ * export const signIn = auth.signIn;
98
+ * export const signOut = auth.signOut;
99
+ * export const store = auth.store;
100
+ * export const http = auth.http;
46
101
  * ```
47
102
  *
48
103
  * @returns An object with fields you should reexport from your
@@ -57,11 +112,12 @@ function Auth(config_) {
57
112
  const getProviderOrThrow = services.providerRegistry.getProviderOrThrow;
58
113
  const INVITE_TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
59
114
  const INVITE_TOKEN_LENGTH = 48;
60
- const GROUP_CONNECTION_ROUTE_BASE = "/api/auth/connections";
115
+ const GROUP_CONNECTION_ROUTE_BASE = "/connections";
61
116
  const group = createGroupService({
62
117
  config,
63
118
  sha256
64
119
  });
120
+ const authRequireEnv = (name) => name === "CONVEX_SITE_URL" ? authSiteUrlFromEnv() : requireEnv(name);
65
121
  const authBase = {
66
122
  ...createCoreDomains({
67
123
  config,
@@ -71,7 +127,26 @@ function Auth(config_) {
71
127
  callModifyAccount,
72
128
  getEnrichCtx: () => enrichCtx,
73
129
  inviteTokenAlphabet: INVITE_TOKEN_ALPHABET,
74
- inviteTokenLength: INVITE_TOKEN_LENGTH
130
+ inviteTokenLength: INVITE_TOKEN_LENGTH,
131
+ signInForProvider: async (ctx, providerConfig, args) => {
132
+ const materialized = typeof providerConfig === "function" ? providerConfig() : providerConfig;
133
+ const result = await services.signIn.signIn(enrichCtx(ctx), materialized, args, {
134
+ generateTokens: false,
135
+ allowExtraProviders: true
136
+ });
137
+ if (result.kind === "signedIn") {
138
+ const session = result.session;
139
+ if (session === null || session === void 0) return null;
140
+ const userId = session.userId;
141
+ const sessionId = session.sessionId ?? session._id;
142
+ if (userId === void 0 || sessionId === void 0) return null;
143
+ return {
144
+ userId,
145
+ sessionId
146
+ };
147
+ }
148
+ throw new FlowSignal(result);
149
+ }
75
150
  }),
76
151
  sso: createGroupConnectionDomain({
77
152
  config,
@@ -82,7 +157,7 @@ function Auth(config_) {
82
157
  emitGroupWebhookDeliveries: group.emitGroupWebhookDeliveries,
83
158
  connectionNotFoundError: "Connection not found.",
84
159
  GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND,
85
- requireEnv,
160
+ requireEnv: authRequireEnv,
86
161
  generateRandomString,
87
162
  INVITE_TOKEN_ALPHABET,
88
163
  sha256,
@@ -91,19 +166,144 @@ function Auth(config_) {
91
166
  })
92
167
  };
93
168
  const getDefaultCorsOrigins = () => siteUrlsFromEnv().allowedUrls.map((u) => new URL(u).origin);
94
- const http = {
95
- add: (http$1) => {
169
+ const getAuthSiteUrl = (_ctx) => authSiteUrlFromEnv();
170
+ const createProtocolRouter = (authSiteUrl) => {
171
+ const protocolHttp = createInMemoryHttpRouter();
172
+ const protocolRequireEnv = (name) => name === "CONVEX_SITE_URL" ? authSiteUrl : requireEnv(name);
173
+ addOpenIdRoutes(protocolHttp, {
174
+ getIssuer: () => authSiteUrl,
175
+ getJwks: () => requireEnv("JWKS")
176
+ });
177
+ addWebAuthnRoute(protocolHttp, { getResponse: () => {
178
+ const r = generateWebAuthnConfig();
179
+ return r === null ? null : {
180
+ body: r.body,
181
+ headers: r.headers
182
+ };
183
+ } });
184
+ addGroupHttpRuntime({
185
+ http: protocolHttp,
186
+ hasSSO,
187
+ auth: authBase,
188
+ config,
189
+ routeBase: "/connections",
190
+ requireEnv: protocolRequireEnv,
191
+ loadActiveConnectionSamlOrThrow: group.loadActiveConnectionSamlOrThrow,
192
+ loadConnectionOidcOrThrow: group.loadConnectionOidcOrThrow,
193
+ getGroupConnectionScimContext: group.getGroupConnectionScimContext,
194
+ loadGroupPolicyOrThrow: group.loadGroupPolicyOrThrow,
195
+ normalizeGroupConnectionPolicy,
196
+ recordGroupAuditEvent: group.recordGroupAuditEvent,
197
+ emitGroupWebhookDeliveries: group.emitGroupWebhookDeliveries,
198
+ generateRandomString,
199
+ inviteTokenAlphabet: INVITE_TOKEN_ALPHABET,
200
+ callUserOAuth,
201
+ callVerifierSignature,
202
+ sharedOidcRedirectURI: ssoProvider?.redirectURI
203
+ });
204
+ if (hasOAuth) addAuthRoutes(protocolHttp, {
205
+ routeBase: "",
206
+ handleSignIn: convertErrorsToResponse(400, async (ctx, request$1) => {
207
+ const url = new URL(request$1.url);
208
+ const pathParts = url.pathname.split("/");
209
+ const providerId = pathParts[pathParts.length - 1];
210
+ if (providerId === null) throw convexError({
211
+ code: "OAUTH_MISSING_PROVIDER",
212
+ message: "Missing OAuth provider ID."
213
+ });
214
+ const verifier = url.searchParams.get("code");
215
+ if (verifier === null) throw convexError({
216
+ code: "OAUTH_MISSING_VERIFIER",
217
+ message: "Missing sign-in verifier."
218
+ });
219
+ const oauthConfig = getProviderOrThrow(providerId);
220
+ const redirectTo = url.searchParams.get("redirectTo");
221
+ const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig, { stateTransform: (state) => encodeOAuthState(state, redirectTo) });
222
+ await callVerifierSignature(ctx, {
223
+ verifier,
224
+ signature
225
+ });
226
+ const headers = new Headers({ Location: redirect });
227
+ for (const { name, value, options } of cookies) headers.append("Set-Cookie", serialize(name, value, options));
228
+ return new Response(null, {
229
+ status: 302,
230
+ headers
231
+ });
232
+ }),
233
+ handleCallback: async (ctx, request$1) => {
234
+ const url = new URL(request$1.url);
235
+ const callbackPathParts = new URL(request$1.url).pathname.split("/");
236
+ const providerId = callbackPathParts[callbackPathParts.length - 1];
237
+ if (!providerId) throw convexError({
238
+ code: "OAUTH_MISSING_PROVIDER",
239
+ message: "Missing OAuth provider ID."
240
+ });
241
+ log(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
242
+ const provider = getProviderOrThrow(providerId);
243
+ const cookies = getCookies(request$1);
244
+ const params = url.searchParams;
245
+ if (request$1.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
246
+ const formData = await request$1.formData();
247
+ for (const [key, value] of formDataEntries(formData)) if (typeof value === "string") params.append(key, value);
248
+ }
249
+ const fallbackDestinationUrl = await redirectAbsoluteUrl(ctx, config, { redirectTo: void 0 });
250
+ try {
251
+ const result = await handleOAuthCallback(providerId, provider, Object.fromEntries(params.entries()), cookies);
252
+ const oauthCookies = result.cookies;
253
+ const { id: profileId, ...profileData } = result.profile;
254
+ const { signature } = result;
255
+ const { redirectTo: stateRedirectTo } = decodeOAuthState(params.get("state") ?? "");
256
+ const redirUrl = setURLSearchParam(await redirectAbsoluteUrl(ctx, config, { redirectTo: stateRedirectTo ?? void 0 }), "code", await callUserOAuth(ctx, {
257
+ provider: providerId,
258
+ providerAccountId: profileId,
259
+ profile: profileData,
260
+ signature
261
+ }));
262
+ const redirHeaders = new Headers({ Location: redirUrl });
263
+ redirHeaders.set("Cache-Control", "must-revalidate");
264
+ for (const { name, value, options } of oauthCookies) redirHeaders.append("Set-Cookie", serialize(name, value, options));
265
+ return new Response(null, {
266
+ status: 302,
267
+ headers: redirHeaders
268
+ });
269
+ } catch (error) {
270
+ logError(error);
271
+ return new Response(null, {
272
+ status: 302,
273
+ headers: { Location: fallbackDestinationUrl }
274
+ });
275
+ }
276
+ }
277
+ });
278
+ return protocolHttp;
279
+ };
280
+ const request = {
281
+ add: (http$1, options) => {
282
+ const routePrefix = normalizeRoutePrefix(options?.prefix);
283
+ const authSiteUrl = () => appendRoutePrefix(requireEnv("CONVEX_SITE_URL"), routePrefix);
284
+ const protocolRequireEnv = (name) => name === "CONVEX_SITE_URL" ? authSiteUrl() : requireEnv(name);
96
285
  addOpenIdRoutes(http$1, {
97
- getIssuer: () => requireEnv("CONVEX_SITE_URL"),
286
+ routeBase: routePrefix,
287
+ getIssuer: authSiteUrl,
98
288
  getJwks: () => requireEnv("JWKS")
99
289
  });
290
+ addWebAuthnRoute(http$1, {
291
+ routeBase: routePrefix,
292
+ getResponse: () => {
293
+ const r = generateWebAuthnConfig();
294
+ return r === null ? null : {
295
+ body: r.body,
296
+ headers: r.headers
297
+ };
298
+ }
299
+ });
100
300
  addGroupHttpRuntime({
101
301
  http: http$1,
102
302
  hasSSO,
103
303
  auth: authBase,
104
304
  config,
105
- routeBase: GROUP_CONNECTION_ROUTE_BASE,
106
- requireEnv,
305
+ routeBase: `${routePrefix}${GROUP_CONNECTION_ROUTE_BASE}`,
306
+ requireEnv: protocolRequireEnv,
107
307
  loadActiveConnectionSamlOrThrow: group.loadActiveConnectionSamlOrThrow,
108
308
  loadConnectionOidcOrThrow: group.loadConnectionOidcOrThrow,
109
309
  getGroupConnectionScimContext: group.getGroupConnectionScimContext,
@@ -118,8 +318,9 @@ function Auth(config_) {
118
318
  sharedOidcRedirectURI: ssoProvider?.redirectURI
119
319
  });
120
320
  if (hasOAuth) addAuthRoutes(http$1, {
121
- handleSignIn: convertErrorsToResponse(400, async (ctx, request) => {
122
- const url = new URL(request.url);
321
+ routeBase: routePrefix,
322
+ handleSignIn: convertErrorsToResponse(400, async (ctx, request$1) => {
323
+ const url = new URL(request$1.url);
123
324
  const pathParts = url.pathname.split("/");
124
325
  const providerId = pathParts[pathParts.length - 1];
125
326
  if (providerId === null) throw convexError({
@@ -131,23 +332,23 @@ function Auth(config_) {
131
332
  code: "OAUTH_MISSING_VERIFIER",
132
333
  message: "Missing sign-in verifier."
133
334
  });
134
- const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, getProviderOrThrow(providerId));
335
+ const oauthConfig = getProviderOrThrow(providerId);
336
+ const redirectTo = url.searchParams.get("redirectTo");
337
+ const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig, { stateTransform: (state) => encodeOAuthState(state, redirectTo) });
135
338
  await callVerifierSignature(ctx, {
136
339
  verifier,
137
340
  signature
138
341
  });
139
- const redirectTo = url.searchParams.get("redirectTo");
140
- if (redirectTo !== null) cookies.push(redirectToParamCookie(providerId, redirectTo));
141
342
  const headers = new Headers({ Location: redirect });
142
- for (const { name, value, options } of cookies) headers.append("Set-Cookie", serialize(name, value, options));
343
+ for (const { name, value, options: options$1 } of cookies) headers.append("Set-Cookie", serialize(name, value, options$1));
143
344
  return new Response(null, {
144
345
  status: 302,
145
346
  headers
146
347
  });
147
348
  }),
148
- handleCallback: async (ctx, request) => {
149
- const url = new URL(request.url);
150
- const callbackPathParts = new URL(request.url).pathname.split("/");
349
+ handleCallback: async (ctx, request$1) => {
350
+ const url = new URL(request$1.url);
351
+ const callbackPathParts = new URL(request$1.url).pathname.split("/");
151
352
  const providerId = callbackPathParts[callbackPathParts.length - 1];
152
353
  if (!providerId) throw convexError({
153
354
  code: "OAUTH_MISSING_PROVIDER",
@@ -155,19 +356,20 @@ function Auth(config_) {
155
356
  });
156
357
  log(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
157
358
  const provider = getProviderOrThrow(providerId);
158
- const cookies = getCookies(request);
159
- const maybeRedirectTo = useRedirectToParam(provider.id, cookies);
160
- const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
359
+ const cookies = getCookies(request$1);
161
360
  const params = url.searchParams;
162
- if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) (await request.formData()).forEach((value, key) => {
163
- if (typeof value === "string") params.append(key, value);
164
- });
361
+ if (request$1.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
362
+ const formData = await request$1.formData();
363
+ for (const [key, value] of formDataEntries(formData)) if (typeof value === "string") params.append(key, value);
364
+ }
365
+ const fallbackDestinationUrl = await redirectAbsoluteUrl(ctx, config, { redirectTo: void 0 });
165
366
  try {
166
367
  const result = await handleOAuthCallback(providerId, provider, Object.fromEntries(params.entries()), cookies);
167
368
  const oauthCookies = result.cookies;
168
369
  const { id: profileId, ...profileData } = result.profile;
169
370
  const { signature } = result;
170
- const redirUrl = setURLSearchParam(destinationUrl, "code", await callUserOAuth(ctx, {
371
+ const { redirectTo: stateRedirectTo } = decodeOAuthState(params.get("state") ?? "");
372
+ const redirUrl = setURLSearchParam(await redirectAbsoluteUrl(ctx, config, { redirectTo: stateRedirectTo ?? void 0 }), "code", await callUserOAuth(ctx, {
171
373
  provider: providerId,
172
374
  providerAccountId: profileId,
173
375
  profile: profileData,
@@ -175,34 +377,126 @@ function Auth(config_) {
175
377
  }));
176
378
  const redirHeaders = new Headers({ Location: redirUrl });
177
379
  redirHeaders.set("Cache-Control", "must-revalidate");
178
- for (const { name, value, options } of [...oauthCookies, ...maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []]) redirHeaders.append("Set-Cookie", serialize(name, value, options));
380
+ for (const { name, value, options: options$1 } of oauthCookies) redirHeaders.append("Set-Cookie", serialize(name, value, options$1));
179
381
  return new Response(null, {
180
382
  status: 302,
181
383
  headers: redirHeaders
182
384
  });
183
385
  } catch (error) {
184
386
  logError(error);
185
- const respHeaders = new Headers({ Location: destinationUrl });
186
- for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) respHeaders.append("Set-Cookie", serialize(name, value, options));
187
387
  return new Response(null, {
188
388
  status: 302,
189
- headers: respHeaders
389
+ headers: { Location: fallbackDestinationUrl }
190
390
  });
191
391
  }
192
392
  }
193
393
  });
194
394
  },
395
+ router: (options) => {
396
+ const http$1 = httpRouter();
397
+ request.add(http$1, options ?? { prefix: "/auth" });
398
+ return http$1;
399
+ },
195
400
  context: createHttpContext(authBase),
196
401
  action: createHttpAction(authBase, getDefaultCorsOrigins),
197
402
  route: createHttpRoute(createHttpAction(authBase, getDefaultCorsOrigins), getDefaultCorsOrigins)
198
403
  };
199
- const auth = Object.assign(authBase, { http });
404
+ const auth = Object.assign(authBase, { request });
405
+ const httpDelegate = actionGeneric({
406
+ args: {
407
+ authSiteUrl: v.string(),
408
+ routePath: v.string(),
409
+ url: v.string(),
410
+ method: v.string(),
411
+ headers: vHeaderPairs,
412
+ body: v.optional(v.string())
413
+ },
414
+ returns: vHttpDelegateResponse,
415
+ handler: async (ctx, args) => {
416
+ const incomingUrl = new URL(args.url);
417
+ const requestUrl = new URL(args.authSiteUrl.replace(/\/$/, "") + args.routePath);
418
+ requestUrl.search = incomingUrl.search;
419
+ const method = args.method.toUpperCase();
420
+ const request$1 = new Request(requestUrl, {
421
+ method,
422
+ headers: new Headers(args.headers),
423
+ body: method === "GET" || method === "HEAD" ? void 0 : args.body ?? ""
424
+ });
425
+ const route = createProtocolRouter(args.authSiteUrl.replace(/\/$/, "")).lookup(args.routePath, method);
426
+ if (!route) return await serializeHttpResponse(new Response(null, { status: 404 }));
427
+ return await serializeHttpResponse(await invokeHttpHandler(route.handler, ctx, request$1));
428
+ }
429
+ });
430
+ const http = Object.assign((options) => request.router(options), httpDelegate);
431
+ const accountUnlink = async (ctx, args) => {
432
+ const accountDoc = await ctx.runQuery(config.component.public.accountGetById, { accountId: args.accountId });
433
+ if (accountDoc === null) throw convexError({
434
+ code: "ACCOUNT_NOT_FOUND",
435
+ message: "Account not found."
436
+ });
437
+ await ctx.runMutation(config.component.public.accountDelete, { accountId: args.accountId });
438
+ const userId = accountDoc.userId;
439
+ const provider = accountDoc.provider;
440
+ await config.callbacks?.after?.(ctx, {
441
+ kind: "accountUnlinked",
442
+ userId,
443
+ accountId: args.accountId,
444
+ provider
445
+ });
446
+ return {
447
+ accountId: args.accountId,
448
+ userId,
449
+ provider
450
+ };
451
+ };
452
+ const passkeyDelete = async (ctx, args) => {
453
+ const passkeyDoc = await ctx.runQuery(config.component.public.passkeyGetById, { passkeyId: args.passkeyId });
454
+ if (passkeyDoc === null) throw convexError({
455
+ code: "PASSKEY_NOT_FOUND",
456
+ message: "Passkey not found."
457
+ });
458
+ await ctx.runMutation(config.component.public.passkeyDelete, { passkeyId: args.passkeyId });
459
+ const userId = passkeyDoc.userId;
460
+ await config.callbacks?.after?.(ctx, {
461
+ kind: "passkeyRemoved",
462
+ userId,
463
+ passkeyId: args.passkeyId
464
+ });
465
+ return {
466
+ passkeyId: args.passkeyId,
467
+ userId
468
+ };
469
+ };
470
+ const totpDelete = async (ctx, args) => {
471
+ const totpDoc = await ctx.runQuery(config.component.public.totpGetById, { totpId: args.totpId });
472
+ if (totpDoc === null) throw convexError({
473
+ code: "TOTP_NOT_FOUND",
474
+ message: "TOTP factor not found."
475
+ });
476
+ await ctx.runMutation(config.component.public.totpDelete, { totpId: args.totpId });
477
+ const userId = totpDoc.userId;
478
+ await config.callbacks?.after?.(ctx, {
479
+ kind: "totpRemoved",
480
+ userId,
481
+ totpId: args.totpId
482
+ });
483
+ return {
484
+ totpId: args.totpId,
485
+ userId
486
+ };
487
+ };
488
+ const enrichedAccount = Object.assign({}, auth.account, { unlink: accountUnlink });
489
+ const passkeyHelpers = { delete: passkeyDelete };
490
+ const totpHelpers = { delete: totpDelete };
200
491
  const enrichCtx = (ctx) => ({
201
492
  ...ctx,
202
493
  auth: {
203
494
  ...ctx.auth,
495
+ getUserIdentity: ctx.auth.getUserIdentity.bind(ctx.auth),
204
496
  config,
205
- account: auth.account,
497
+ account: enrichedAccount,
498
+ passkey: passkeyHelpers,
499
+ totp: totpHelpers,
206
500
  session: auth.session,
207
501
  member: auth.member,
208
502
  provider: auth.provider
@@ -221,9 +515,11 @@ function Auth(config_) {
221
515
  handler: async (ctx, args) => {
222
516
  if (args.calledBy !== void 0) log("INFO", `\`auth:signIn\` called by ${args.calledBy}`);
223
517
  const provider = args.provider !== void 0 ? getProviderOrThrow(args.provider) : null;
518
+ const authSiteUrl = provider?.type === "oauth" || provider?.type === "sso" ? getAuthSiteUrl(ctx) : void 0;
224
519
  const result = await services.signIn.signIn(enrichCtx(ctx), provider, args, {
225
520
  generateTokens: true,
226
521
  allowExtraProviders: false,
522
+ authSiteUrl,
227
523
  resolveSsoProtocol: group.resolveGroupConnectionSsoProtocolOrThrow
228
524
  });
229
525
  const handler = {
@@ -253,7 +549,8 @@ function Auth(config_) {
253
549
  handler: async (ctx, args) => {
254
550
  return storeImpl(ctx, args, services);
255
551
  }
256
- })
552
+ }),
553
+ http
257
554
  };
258
555
  }
259
556
 
@@ -182,6 +182,10 @@ function createGroupService(deps) {
182
182
  code: "INVALID_PARAMETERS",
183
183
  message: connectionNotFoundError
184
184
  });
185
+ if (connection.status !== "active") throw convexError({
186
+ code: "INVALID_PARAMETERS",
187
+ message: "Connection is not active."
188
+ });
185
189
  return {
186
190
  scimConfig,
187
191
  connection,
@@ -9,6 +9,7 @@ import { generateToken } from "./tokens.js";
9
9
 
10
10
  //#region src/server/sessions.ts
11
11
  const DEFAULT_SESSION_TOTAL_DURATION_MS = 1e3 * 60 * 60 * 24 * 30;
12
+ /** @internal */
12
13
  const sessionExpirationTime = (config, now = Date.now()) => now + (config.session?.totalDurationMs ?? readConfigSync(envOptionalNumber("AUTH_SESSION_TOTAL_DURATION_MS")) ?? DEFAULT_SESSION_TOTAL_DURATION_MS);
13
14
  const encodeRefreshToken = (refreshTokenId, sessionId) => `${refreshTokenId}${REFRESH_TOKEN_DIVIDER}${sessionId}`;
14
15
  function buildSessionIdentity(userId, sessionId, user) {
@@ -8,9 +8,9 @@ import { callRefreshSession } from "./mutations/refresh.js";
8
8
  import { callSignIn } from "./mutations/signin.js";
9
9
  import { callVerifier } from "./mutations/verifier.js";
10
10
  import { callVerifyCodeAndSignIn } from "./mutations/verify.js";
11
- import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
12
11
  import { authFlowError } from "../shared/errors.js";
13
- import { toConvexError } from "./errors.js";
12
+ import { FlowSignal, toConvexError } from "./errors.js";
13
+ import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
14
14
  import { queryTotpVerifiedByUserId } from "./types.js";
15
15
  import { handleDevice } from "./device.js";
16
16
  import { handlePasskeyFx } from "./passkey.js";
@@ -146,7 +146,7 @@ async function handleEmailAndPhoneProviderFx(ctx, provider, args, options) {
146
146
  }
147
147
  let destination;
148
148
  try {
149
- destination = await redirectAbsoluteUrl(ctx.auth.config, args.params ?? {});
149
+ destination = await redirectAbsoluteUrl(ctx, ctx.auth.config, args.params ?? {});
150
150
  } catch (error) {
151
151
  throw asConvexError(error, "INVALID_REDIRECT", "Failed to resolve redirect URL.");
152
152
  }
@@ -182,6 +182,7 @@ async function handleCredentialsFx(ctx, provider, args, options) {
182
182
  try {
183
183
  result = await withSpan("convex-auth.signin.credentials.authorize", { providerId: provider.id }, () => provider.authorize(args.params ?? {}, ctx));
184
184
  } catch (error) {
185
+ if (error instanceof FlowSignal) return error.result;
185
186
  throw asCredentialsError(error);
186
187
  }
187
188
  if (result === null) return {
@@ -272,7 +273,8 @@ async function handleOAuthProviderFx(ctx, provider, args, options) {
272
273
  session: result
273
274
  };
274
275
  }
275
- const redirect = new URL((readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL")) + `/api/auth/signin/${provider.id}`);
276
+ const authSiteUrl = options.authSiteUrl ?? readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL");
277
+ const redirect = new URL(`${authSiteUrl.replace(/\/$/, "")}/signin/${provider.id}`);
276
278
  let verifier;
277
279
  try {
278
280
  verifier = await callVerifier(ctx);
@@ -315,8 +317,8 @@ async function handleSsoProviderFx(ctx, args, options) {
315
317
  } catch (error) {
316
318
  throw asConvexError(error, "INTERNAL_ERROR", "Failed to create verifier.");
317
319
  }
318
- const siteUrl = readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL");
319
- const redirect = new URL(`${siteUrl}/api/auth/connections/${connectionId}/${protocol}/signin`);
320
+ const siteUrl = options.authSiteUrl ?? readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL");
321
+ const redirect = new URL(`${siteUrl.replace(/\/$/, "")}/connections/${connectionId}/${protocol}/signin`);
320
322
  redirect.searchParams.set("code", verifier);
321
323
  if (typeof args.params?.redirectTo === "string") redirect.searchParams.set("redirectTo", args.params.redirectTo);
322
324
  if (typeof normalizedParams.loginHint === "string") redirect.searchParams.set("loginHint", normalizedParams.loginHint);