@robelest/convex-auth 0.0.4-preview.13 → 0.0.4-preview.16

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 (328) hide show
  1. package/README.md +140 -9
  2. package/dist/bin.cjs +5957 -5478
  3. package/dist/client/index.d.ts +3 -7
  4. package/dist/client/index.d.ts.map +1 -1
  5. package/dist/client/index.js +27 -26
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/component/_generated/api.d.ts +14 -0
  8. package/dist/component/_generated/api.d.ts.map +1 -1
  9. package/dist/component/_generated/api.js.map +1 -1
  10. package/dist/component/_generated/component.d.ts +1672 -24
  11. package/dist/component/_generated/component.d.ts.map +1 -1
  12. package/dist/component/convex.config.d.ts +2 -2
  13. package/dist/component/convex.config.d.ts.map +1 -1
  14. package/dist/component/index.d.ts +1 -1
  15. package/dist/component/index.js +2 -2
  16. package/dist/component/model.d.ts +153 -0
  17. package/dist/component/model.d.ts.map +1 -0
  18. package/dist/component/model.js +343 -0
  19. package/dist/component/model.js.map +1 -0
  20. package/dist/component/providers/sso.d.ts +1 -1
  21. package/dist/component/public/enterprise.d.ts +54 -0
  22. package/dist/component/public/enterprise.d.ts.map +1 -0
  23. package/dist/component/public/enterprise.js +515 -0
  24. package/dist/component/public/enterprise.js.map +1 -0
  25. package/dist/component/public/factors.d.ts +52 -0
  26. package/dist/component/public/factors.d.ts.map +1 -0
  27. package/dist/component/public/factors.js +285 -0
  28. package/dist/component/public/factors.js.map +1 -0
  29. package/dist/component/public/groups.d.ts +116 -0
  30. package/dist/component/public/groups.d.ts.map +1 -0
  31. package/dist/component/public/groups.js +596 -0
  32. package/dist/component/public/groups.js.map +1 -0
  33. package/dist/component/public/identity.d.ts +93 -0
  34. package/dist/component/public/identity.d.ts.map +1 -0
  35. package/dist/component/public/identity.js +426 -0
  36. package/dist/component/public/identity.js.map +1 -0
  37. package/dist/component/public/keys.d.ts +41 -0
  38. package/dist/component/public/keys.d.ts.map +1 -0
  39. package/dist/component/public/keys.js +157 -0
  40. package/dist/component/public/keys.js.map +1 -0
  41. package/dist/component/public/shared.d.ts +26 -0
  42. package/dist/component/public/shared.d.ts.map +1 -0
  43. package/dist/component/public/shared.js +32 -0
  44. package/dist/component/public/shared.js.map +1 -0
  45. package/dist/component/public.d.ts +9 -321
  46. package/dist/component/public.d.ts.map +1 -1
  47. package/dist/component/public.js +6 -2145
  48. package/dist/component/schema.d.ts +406 -260
  49. package/dist/component/schema.js +37 -32
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/server/auth.d.ts +161 -15
  52. package/dist/component/server/auth.d.ts.map +1 -1
  53. package/dist/component/server/auth.js +100 -7
  54. package/dist/component/server/auth.js.map +1 -1
  55. package/dist/component/server/cookies.js +3 -0
  56. package/dist/component/server/cookies.js.map +1 -1
  57. package/dist/component/server/db.js +1 -0
  58. package/dist/component/server/db.js.map +1 -1
  59. package/dist/component/server/device.js +3 -1
  60. package/dist/component/server/device.js.map +1 -1
  61. package/dist/component/server/domains/core.js +629 -0
  62. package/dist/component/server/domains/core.js.map +1 -0
  63. package/dist/component/server/domains/sso.js +884 -0
  64. package/dist/component/server/domains/sso.js.map +1 -0
  65. package/dist/component/server/factory.d.ts +136 -0
  66. package/dist/component/server/factory.d.ts.map +1 -0
  67. package/dist/component/server/factory.js +1134 -0
  68. package/dist/component/server/factory.js.map +1 -0
  69. package/dist/component/server/fx.js +2 -1
  70. package/dist/component/server/fx.js.map +1 -1
  71. package/dist/component/server/http.js +287 -0
  72. package/dist/component/server/http.js.map +1 -0
  73. package/dist/component/server/identity.js +13 -0
  74. package/dist/component/server/identity.js.map +1 -0
  75. package/dist/component/server/keys.js +4 -0
  76. package/dist/component/server/keys.js.map +1 -1
  77. package/dist/component/server/mutations/account.js +1 -1
  78. package/dist/component/server/mutations/index.js +2 -2
  79. package/dist/component/server/mutations/index.js.map +1 -1
  80. package/dist/component/server/mutations/invalidate.js +1 -1
  81. package/dist/component/server/mutations/oauth.js +10 -7
  82. package/dist/component/server/mutations/oauth.js.map +1 -1
  83. package/dist/component/server/mutations/refresh.js +1 -1
  84. package/dist/component/server/mutations/register.js +1 -1
  85. package/dist/component/server/mutations/retrieve.js +1 -1
  86. package/dist/component/server/mutations/signature.js +1 -1
  87. package/dist/component/server/mutations/store.js +6 -3
  88. package/dist/component/server/mutations/store.js.map +1 -1
  89. package/dist/component/server/mutations/verify.js +1 -1
  90. package/dist/component/server/oauth.js +3 -0
  91. package/dist/component/server/oauth.js.map +1 -1
  92. package/dist/component/server/passkey.js +3 -2
  93. package/dist/component/server/passkey.js.map +1 -1
  94. package/dist/component/server/provider.js +2 -0
  95. package/dist/component/server/provider.js.map +1 -1
  96. package/dist/component/server/providers.js +10 -0
  97. package/dist/component/server/providers.js.map +1 -1
  98. package/dist/component/server/ratelimit.js +3 -0
  99. package/dist/component/server/ratelimit.js.map +1 -1
  100. package/dist/component/server/redirects.js +2 -0
  101. package/dist/component/server/redirects.js.map +1 -1
  102. package/dist/component/server/refresh.js +5 -0
  103. package/dist/component/server/refresh.js.map +1 -1
  104. package/dist/component/server/sessions.js +5 -0
  105. package/dist/component/server/sessions.js.map +1 -1
  106. package/dist/component/server/signin.js +2 -1
  107. package/dist/component/server/signin.js.map +1 -1
  108. package/dist/component/server/sso.js +166 -19
  109. package/dist/component/server/sso.js.map +1 -1
  110. package/dist/component/server/tokens.js +1 -0
  111. package/dist/component/server/tokens.js.map +1 -1
  112. package/dist/component/server/totp.js +4 -2
  113. package/dist/component/server/totp.js.map +1 -1
  114. package/dist/component/server/types.d.ts +106 -38
  115. package/dist/component/server/types.d.ts.map +1 -1
  116. package/dist/component/server/types.js.map +1 -1
  117. package/dist/component/server/users.js +1 -0
  118. package/dist/component/server/users.js.map +1 -1
  119. package/dist/component/server/utils.js +44 -2
  120. package/dist/component/server/utils.js.map +1 -1
  121. package/dist/providers/anonymous.d.ts +1 -1
  122. package/dist/providers/credentials.d.ts +1 -1
  123. package/dist/providers/password.d.ts +1 -1
  124. package/dist/providers/sso.d.ts +1 -1
  125. package/dist/providers/sso.js.map +1 -1
  126. package/dist/server/auth.d.ts +163 -17
  127. package/dist/server/auth.d.ts.map +1 -1
  128. package/dist/server/auth.js +100 -7
  129. package/dist/server/auth.js.map +1 -1
  130. package/dist/server/cookies.d.ts +1 -38
  131. package/dist/server/cookies.js +3 -0
  132. package/dist/server/cookies.js.map +1 -1
  133. package/dist/server/db.d.ts +1 -125
  134. package/dist/server/db.js +1 -0
  135. package/dist/server/db.js.map +1 -1
  136. package/dist/server/device.d.ts +1 -24
  137. package/dist/server/device.js +3 -1
  138. package/dist/server/device.js.map +1 -1
  139. package/dist/server/domains/core.d.ts +434 -0
  140. package/dist/server/domains/core.d.ts.map +1 -0
  141. package/dist/server/domains/core.js +629 -0
  142. package/dist/server/domains/core.js.map +1 -0
  143. package/dist/server/domains/sso.d.ts +409 -0
  144. package/dist/server/domains/sso.d.ts.map +1 -0
  145. package/dist/server/domains/sso.js +884 -0
  146. package/dist/server/domains/sso.js.map +1 -0
  147. package/dist/server/enterpriseValidators.d.ts +1 -0
  148. package/dist/server/enterpriseValidators.js +60 -0
  149. package/dist/server/enterpriseValidators.js.map +1 -0
  150. package/dist/server/factory.d.ts +136 -0
  151. package/dist/server/factory.d.ts.map +1 -0
  152. package/dist/server/factory.js +1134 -0
  153. package/dist/server/factory.js.map +1 -0
  154. package/dist/server/fx.d.ts +1 -16
  155. package/dist/server/fx.d.ts.map +1 -1
  156. package/dist/server/fx.js +1 -0
  157. package/dist/server/fx.js.map +1 -1
  158. package/dist/server/http.d.ts +59 -0
  159. package/dist/server/http.d.ts.map +1 -0
  160. package/dist/server/http.js +287 -0
  161. package/dist/server/http.js.map +1 -0
  162. package/dist/server/identity.d.ts +1 -0
  163. package/dist/server/identity.js +13 -0
  164. package/dist/server/identity.js.map +1 -0
  165. package/dist/server/index.d.ts +468 -1
  166. package/dist/server/index.d.ts.map +1 -1
  167. package/dist/server/index.js +530 -36
  168. package/dist/server/index.js.map +1 -1
  169. package/dist/server/keys.d.ts +1 -57
  170. package/dist/server/keys.js +4 -0
  171. package/dist/server/keys.js.map +1 -1
  172. package/dist/server/mutations/account.d.ts +7 -7
  173. package/dist/server/mutations/account.d.ts.map +1 -1
  174. package/dist/server/mutations/code.d.ts +13 -13
  175. package/dist/server/mutations/code.d.ts.map +1 -1
  176. package/dist/server/mutations/index.d.ts +107 -107
  177. package/dist/server/mutations/index.d.ts.map +1 -1
  178. package/dist/server/mutations/index.js +1 -1
  179. package/dist/server/mutations/index.js.map +1 -1
  180. package/dist/server/mutations/invalidate.d.ts +5 -5
  181. package/dist/server/mutations/invalidate.d.ts.map +1 -1
  182. package/dist/server/mutations/oauth.d.ts +10 -10
  183. package/dist/server/mutations/oauth.d.ts.map +1 -1
  184. package/dist/server/mutations/oauth.js +9 -6
  185. package/dist/server/mutations/oauth.js.map +1 -1
  186. package/dist/server/mutations/refresh.d.ts +4 -4
  187. package/dist/server/mutations/register.d.ts +12 -12
  188. package/dist/server/mutations/register.d.ts.map +1 -1
  189. package/dist/server/mutations/retrieve.d.ts +7 -7
  190. package/dist/server/mutations/signature.d.ts +5 -5
  191. package/dist/server/mutations/signin.d.ts +6 -6
  192. package/dist/server/mutations/signin.d.ts.map +1 -1
  193. package/dist/server/mutations/signout.d.ts +1 -1
  194. package/dist/server/mutations/store.d.ts +3 -2
  195. package/dist/server/mutations/store.d.ts.map +1 -1
  196. package/dist/server/mutations/store.js +6 -3
  197. package/dist/server/mutations/store.js.map +1 -1
  198. package/dist/server/mutations/verifier.d.ts +1 -1
  199. package/dist/server/mutations/verify.d.ts +11 -11
  200. package/dist/server/mutations/verify.d.ts.map +1 -1
  201. package/dist/server/oauth.d.ts +1 -59
  202. package/dist/server/oauth.js +3 -0
  203. package/dist/server/oauth.js.map +1 -1
  204. package/dist/server/passkey.d.ts.map +1 -1
  205. package/dist/server/passkey.js +3 -2
  206. package/dist/server/passkey.js.map +1 -1
  207. package/dist/server/provider.d.ts +1 -14
  208. package/dist/server/provider.d.ts.map +1 -1
  209. package/dist/server/provider.js +2 -0
  210. package/dist/server/provider.js.map +1 -1
  211. package/dist/server/providers.js +10 -0
  212. package/dist/server/providers.js.map +1 -1
  213. package/dist/server/ratelimit.d.ts +1 -22
  214. package/dist/server/ratelimit.js +3 -0
  215. package/dist/server/ratelimit.js.map +1 -1
  216. package/dist/server/redirects.d.ts +1 -10
  217. package/dist/server/redirects.js +2 -0
  218. package/dist/server/redirects.js.map +1 -1
  219. package/dist/server/refresh.d.ts +1 -37
  220. package/dist/server/refresh.js +5 -0
  221. package/dist/server/refresh.js.map +1 -1
  222. package/dist/server/sessions.d.ts +1 -28
  223. package/dist/server/sessions.js +5 -0
  224. package/dist/server/sessions.js.map +1 -1
  225. package/dist/server/signin.d.ts +1 -55
  226. package/dist/server/signin.js +2 -1
  227. package/dist/server/signin.js.map +1 -1
  228. package/dist/server/sso.d.ts +1 -348
  229. package/dist/server/sso.js +165 -18
  230. package/dist/server/sso.js.map +1 -1
  231. package/dist/server/templates.d.ts +1 -21
  232. package/dist/server/templates.js +1 -0
  233. package/dist/server/templates.js.map +1 -1
  234. package/dist/server/tokens.d.ts +1 -11
  235. package/dist/server/tokens.js +1 -0
  236. package/dist/server/tokens.js.map +1 -1
  237. package/dist/server/totp.d.ts +1 -23
  238. package/dist/server/totp.js +4 -2
  239. package/dist/server/totp.js.map +1 -1
  240. package/dist/server/types.d.ts +114 -77
  241. package/dist/server/types.d.ts.map +1 -1
  242. package/dist/server/types.js.map +1 -1
  243. package/dist/server/users.d.ts +1 -31
  244. package/dist/server/users.js +1 -0
  245. package/dist/server/users.js.map +1 -1
  246. package/dist/server/utils.d.ts +1 -27
  247. package/dist/server/utils.js +44 -2
  248. package/dist/server/utils.js.map +1 -1
  249. package/dist/server/version.d.ts +1 -1
  250. package/dist/server/version.js +1 -1
  251. package/dist/server/version.js.map +1 -1
  252. package/package.json +4 -5
  253. package/src/cli/bin.ts +5 -0
  254. package/src/cli/index.ts +22 -9
  255. package/src/cli/keys.ts +3 -0
  256. package/src/client/index.ts +36 -37
  257. package/src/component/_generated/api.ts +14 -0
  258. package/src/component/_generated/component.ts +2106 -9
  259. package/src/component/index.ts +3 -1
  260. package/src/component/model.ts +441 -0
  261. package/src/component/public/enterprise.ts +753 -0
  262. package/src/component/public/factors.ts +332 -0
  263. package/src/component/public/groups.ts +932 -0
  264. package/src/component/public/identity.ts +566 -0
  265. package/src/component/public/keys.ts +209 -0
  266. package/src/component/public/shared.ts +119 -0
  267. package/src/component/public.ts +5 -2965
  268. package/src/component/schema.ts +68 -63
  269. package/src/providers/sso.ts +1 -1
  270. package/src/server/auth.ts +413 -18
  271. package/src/server/cookies.ts +3 -0
  272. package/src/server/db.ts +3 -0
  273. package/src/server/device.ts +3 -1
  274. package/src/server/domains/core.ts +1071 -0
  275. package/src/server/domains/sso.ts +1749 -0
  276. package/src/server/enterpriseValidators.ts +93 -0
  277. package/src/server/factory.ts +2181 -0
  278. package/src/server/fx.ts +1 -0
  279. package/src/server/http.ts +529 -0
  280. package/src/server/identity.ts +18 -0
  281. package/src/server/index.ts +806 -40
  282. package/src/server/keys.ts +4 -0
  283. package/src/server/mutations/index.ts +1 -1
  284. package/src/server/mutations/oauth.ts +36 -8
  285. package/src/server/mutations/store.ts +6 -3
  286. package/src/server/oauth.ts +6 -0
  287. package/src/server/passkey.ts +3 -2
  288. package/src/server/provider.ts +2 -0
  289. package/src/server/providers.ts +20 -0
  290. package/src/server/ratelimit.ts +3 -0
  291. package/src/server/redirects.ts +2 -0
  292. package/src/server/refresh.ts +5 -0
  293. package/src/server/sessions.ts +5 -0
  294. package/src/server/signin.ts +1 -0
  295. package/src/server/sso.ts +259 -17
  296. package/src/server/templates.ts +1 -0
  297. package/src/server/tokens.ts +1 -0
  298. package/src/server/totp.ts +4 -2
  299. package/src/server/types.ts +178 -83
  300. package/src/server/users.ts +1 -0
  301. package/src/server/utils.ts +71 -1
  302. package/src/server/version.ts +1 -1
  303. package/dist/component/public.js.map +0 -1
  304. package/dist/component/server/implementation.d.ts +0 -1264
  305. package/dist/component/server/implementation.d.ts.map +0 -1
  306. package/dist/component/server/implementation.js +0 -2365
  307. package/dist/component/server/implementation.js.map +0 -1
  308. package/dist/server/cookies.d.ts.map +0 -1
  309. package/dist/server/db.d.ts.map +0 -1
  310. package/dist/server/device.d.ts.map +0 -1
  311. package/dist/server/implementation.d.ts +0 -1264
  312. package/dist/server/implementation.d.ts.map +0 -1
  313. package/dist/server/implementation.js +0 -2365
  314. package/dist/server/implementation.js.map +0 -1
  315. package/dist/server/keys.d.ts.map +0 -1
  316. package/dist/server/oauth.d.ts.map +0 -1
  317. package/dist/server/ratelimit.d.ts.map +0 -1
  318. package/dist/server/redirects.d.ts.map +0 -1
  319. package/dist/server/refresh.d.ts.map +0 -1
  320. package/dist/server/sessions.d.ts.map +0 -1
  321. package/dist/server/signin.d.ts.map +0 -1
  322. package/dist/server/sso.d.ts.map +0 -1
  323. package/dist/server/templates.d.ts.map +0 -1
  324. package/dist/server/tokens.d.ts.map +0 -1
  325. package/dist/server/totp.d.ts.map +0 -1
  326. package/dist/server/users.d.ts.map +0 -1
  327. package/dist/server/utils.d.ts.map +0 -1
  328. package/src/server/implementation.ts +0 -5336
@@ -0,0 +1,2181 @@
1
+ import {
2
+ Auth,
3
+ GenericActionCtx,
4
+ GenericDataModel,
5
+ HttpRouter,
6
+ actionGeneric,
7
+ internalMutationGeneric,
8
+ } from "convex/server";
9
+ import { v } from "convex/values";
10
+ import { serialize as serializeCookie } from "cookie";
11
+
12
+ import { redirectToParamCookie, useRedirectToParam } from "./cookies";
13
+ import { createCoreDomains } from "./domains/core";
14
+ import { createSsoDomain } from "./domains/sso";
15
+ import { isAuthError } from "./errors";
16
+ import { AuthError, Fx } from "./fx";
17
+ import {
18
+ addAuthRoutes,
19
+ addOpenIdRoutes,
20
+ addSSORoutes,
21
+ convertErrorsToResponse,
22
+ createHttpAction,
23
+ createHttpRoute,
24
+ getCookies,
25
+ type SSORuntimeRoute,
26
+ } from "./http";
27
+ import {
28
+ callCreateAccountFromCredentials,
29
+ callInvalidateSessions,
30
+ callModifyAccount,
31
+ callRetrieveAccountWithCredentials,
32
+ callSignOut,
33
+ callUserOAuth,
34
+ callVerifierSignature,
35
+ storeArgs,
36
+ storeImpl,
37
+ } from "./mutations/index";
38
+ import { createOAuthAuthorizationURL, handleOAuthCallback } from "./oauth";
39
+ import { GetProviderOrThrowFunc } from "./provider";
40
+ import { configDefaults, listAvailableProviders } from "./providers";
41
+ import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects";
42
+ import { signInImpl } from "./signin";
43
+ import {
44
+ createEnterpriseSamlMetadataXml,
45
+ createEnterpriseSamlSignInRequest,
46
+ createEnterpriseOidcRuntime,
47
+ createServiceProviderMetadata,
48
+ createSamlPostBindingResponse,
49
+ encodeEnterpriseSamlRelayState,
50
+ enterpriseOidcProviderId,
51
+ enterpriseSamlProviderId,
52
+ getEnterpriseOidcUrls,
53
+ getPublicOidcConfig,
54
+ getSamlServiceProviderOptions,
55
+ isEnterpriseSamlSourceActive,
56
+ parseEnterpriseSamlLoginResponse,
57
+ parseEnterpriseSamlLogoutMessage,
58
+ getOidcConfig,
59
+ getSamlConfig,
60
+ normalizeEnterprisePolicy,
61
+ normalizeDomain,
62
+ patchEnterprisePolicy,
63
+ parseScimListRequest,
64
+ parseScimPath,
65
+ parseSamlIdpMetadata,
66
+ profileFromSamlExtract,
67
+ SCIM_GROUP_SCHEMA_ID,
68
+ SCIM_USER_SCHEMA_ID,
69
+ scimError,
70
+ scimJson,
71
+ serializeScimGroup,
72
+ serializeScimUser,
73
+ upsertProtocolConfig,
74
+ validateEnterpriseSamlLoginRelayState,
75
+ withOidcSecretState,
76
+ } from "./sso";
77
+ import type {
78
+ ConvexAuthConfig,
79
+ FunctionReferenceFromExport,
80
+ OAuthMaterializedConfig,
81
+ Tokens,
82
+ } from "./types";
83
+ import { MutationCtx } from "./types";
84
+ import {
85
+ decryptSecret,
86
+ encryptSecret,
87
+ generateRandomString,
88
+ LOG_LEVELS,
89
+ logError,
90
+ logWithLevel,
91
+ sha256,
92
+ } from "./utils";
93
+ import { requireEnv } from "./utils";
94
+
95
+ const ENTERPRISE_OIDC_CLIENT_SECRET_KIND = "oidc_client_secret" as const;
96
+
97
+ /**
98
+ * The type of the signIn Convex Action returned from the auth() helper.
99
+ *
100
+ * This type is exported for implementors of other client integrations.
101
+ * However it is not stable, and may change until this library reaches 1.0.
102
+ *
103
+ * @internal
104
+ */
105
+ export type SignInAction = FunctionReferenceFromExport<
106
+ ReturnType<typeof Auth>["signIn"]
107
+ >;
108
+
109
+ /** @internal */
110
+ export type SignInActionResult =
111
+ | { kind: "signedIn"; tokens: Tokens | null }
112
+ | { kind: "redirect"; redirect: string; verifier: string }
113
+ | { kind: "started" }
114
+ | { kind: "passkeyOptions"; options: Record<string, any>; verifier: string }
115
+ | { kind: "totpRequired"; verifier: string }
116
+ | {
117
+ kind: "totpSetup";
118
+ totpSetup: { uri: string; secret: string; totpId: string };
119
+ verifier: string;
120
+ }
121
+ | {
122
+ kind: "deviceCode";
123
+ deviceCode: {
124
+ deviceCode: string;
125
+ userCode: string;
126
+ verificationUri: string;
127
+ verificationUriComplete: string;
128
+ expiresIn: number;
129
+ interval: number;
130
+ };
131
+ };
132
+ /**
133
+ * The type of the signOut Convex Action returned from the auth() helper.
134
+ *
135
+ * This type is exported for implementors of other client integrations.
136
+ * However it is not stable, and may change until this library reaches 1.0.
137
+ *
138
+ * @internal
139
+ */
140
+ export type SignOutAction = FunctionReferenceFromExport<
141
+ ReturnType<typeof Auth>["signOut"]
142
+ >;
143
+
144
+ /**
145
+ * Configure the Convex Auth library. Returns an object with
146
+ * functions and `auth` helper. You must export the functions
147
+ * from `convex/auth.ts` to make them callable:
148
+ *
149
+ * ```ts filename="convex/auth.ts"
150
+ * import { createAuth } from "@robelest/convex-auth/component";
151
+ * import { components } from "./_generated/api";
152
+ *
153
+ * export const auth = createAuth(components.auth, {
154
+ * providers: [],
155
+ * });
156
+ * export const { signIn, signOut, store } = auth;
157
+ * ```
158
+ *
159
+ * @returns An object with fields you should reexport from your
160
+ * `convex/auth.ts` file.
161
+ */
162
+ export function Auth(config_: ConvexAuthConfig) {
163
+ const config = configDefaults(config_);
164
+ const hasOAuth = config.providers.some(
165
+ (provider) => provider.type === "oauth",
166
+ );
167
+ const hasSSO = config.providers.some((provider) => provider.type === "sso");
168
+ const getProviderOrThrow: GetProviderOrThrowFunc = (
169
+ id: string,
170
+ allowExtraProviders: boolean = false,
171
+ ) => {
172
+ const provider =
173
+ config.providers.find(
174
+ (configuredProvider) => configuredProvider.id === id,
175
+ ) ??
176
+ (allowExtraProviders
177
+ ? config.extraProviders.find(
178
+ (configuredProvider) => configuredProvider.id === id,
179
+ )
180
+ : undefined);
181
+ if (provider === undefined) {
182
+ const detail =
183
+ `Provider \`${id}\` is not configured, ` +
184
+ `available providers are ${listAvailableProviders(config, allowExtraProviders)}.`;
185
+ logWithLevel(LOG_LEVELS.ERROR, detail);
186
+ throw new AuthError("PROVIDER_NOT_CONFIGURED", detail, {
187
+ provider: id,
188
+ }).toConvexError();
189
+ }
190
+ return provider;
191
+ };
192
+ type ComponentCtx = Pick<
193
+ GenericActionCtx<GenericDataModel>,
194
+ "runQuery" | "runMutation"
195
+ >;
196
+ type ComponentReadCtx = Pick<GenericActionCtx<GenericDataModel>, "runQuery">;
197
+ const getEnterpriseSecret = async (
198
+ ctx: ComponentReadCtx | ComponentCtx,
199
+ enterpriseId: string,
200
+ kind: typeof ENTERPRISE_OIDC_CLIENT_SECRET_KIND,
201
+ ) => {
202
+ return await ctx.runQuery(config.component.public.enterpriseSecretGet, {
203
+ enterpriseId,
204
+ kind,
205
+ });
206
+ };
207
+ const getEnterpriseOidcConfigWithSecret = async (
208
+ ctx: ComponentReadCtx | ComponentCtx,
209
+ enterprise: { _id: string; config?: unknown },
210
+ ): Promise<Record<string, any>> => {
211
+ const oidc = getOidcConfig(enterprise.config);
212
+ const secret = await getEnterpriseSecret(
213
+ ctx,
214
+ enterprise._id,
215
+ ENTERPRISE_OIDC_CLIENT_SECRET_KIND,
216
+ );
217
+ return {
218
+ ...oidc,
219
+ ...(secret
220
+ ? { clientSecret: await decryptSecret(secret.ciphertext) }
221
+ : {}),
222
+ };
223
+ };
224
+ const INVITE_TOKEN_ALPHABET =
225
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
226
+ const INVITE_TOKEN_LENGTH = 48;
227
+
228
+ const enterpriseNotFoundError = "Enterprise not found.";
229
+
230
+ const ENTERPRISE_CONTROL_ROUTE_BASE = "/api/auth/sso";
231
+
232
+ const getPolicyFromEnterprise = (enterprise: { policy?: unknown }) =>
233
+ normalizeEnterprisePolicy(enterprise.policy);
234
+
235
+ const loadEnterpriseOrThrow = async (
236
+ ctx: ComponentReadCtx,
237
+ enterpriseId: string,
238
+ ) => {
239
+ const enterprise = await ctx.runQuery(
240
+ config.component.public.enterpriseGet,
241
+ {
242
+ enterpriseId,
243
+ },
244
+ );
245
+ if (!enterprise) {
246
+ throw new AuthError(
247
+ "INVALID_PARAMETERS",
248
+ enterpriseNotFoundError,
249
+ ).toConvexError();
250
+ }
251
+ return enterprise;
252
+ };
253
+
254
+ const loadActiveEnterpriseOrThrow = async (
255
+ ctx: ComponentReadCtx,
256
+ enterpriseId: string,
257
+ ) => {
258
+ const enterprise = await loadEnterpriseOrThrow(ctx, enterpriseId);
259
+ if (enterprise.status !== "active") {
260
+ throw new AuthError(
261
+ "INVALID_PARAMETERS",
262
+ "Enterprise connection is not active.",
263
+ ).toConvexError();
264
+ }
265
+ return enterprise;
266
+ };
267
+
268
+ const loadActiveEnterpriseSamlOrThrow = async (
269
+ ctx: ComponentReadCtx,
270
+ enterpriseId: string,
271
+ ) => {
272
+ const enterprise = await loadEnterpriseOrThrow(ctx, enterpriseId);
273
+ const loaded = {
274
+ source: {
275
+ kind: "enterprise" as const,
276
+ id: enterpriseId,
277
+ },
278
+ config: enterprise.config,
279
+ status: enterprise.status,
280
+ enterprise,
281
+ };
282
+ if (!isEnterpriseSamlSourceActive(loaded)) {
283
+ throw new AuthError(
284
+ "INVALID_PARAMETERS",
285
+ "Enterprise connection is not active.",
286
+ ).toConvexError();
287
+ }
288
+ const saml = getSamlConfig(loaded.config);
289
+ if (!saml.idp?.metadataXml) {
290
+ throw new AuthError(
291
+ "PROVIDER_NOT_CONFIGURED",
292
+ "SAML is not configured for this enterprise.",
293
+ ).toConvexError();
294
+ }
295
+ return { loaded, enterprise, saml };
296
+ };
297
+
298
+ const loadEnterpriseOidcOrThrow = async (
299
+ ctx: ComponentReadCtx,
300
+ enterpriseId: string,
301
+ ) => {
302
+ const enterprise = await loadActiveEnterpriseOrThrow(ctx, enterpriseId);
303
+ const oidc = await getEnterpriseOidcConfigWithSecret(ctx, enterprise);
304
+ if (oidc.enabled !== true) {
305
+ throw new AuthError(
306
+ "PROVIDER_NOT_CONFIGURED",
307
+ "OIDC is not configured for this enterprise.",
308
+ ).toConvexError();
309
+ }
310
+ return { enterprise, oidc };
311
+ };
312
+
313
+ const validateEnterprisePolicy = (
314
+ policy: ReturnType<typeof normalizeEnterprisePolicy>,
315
+ ) => {
316
+ const checks: Array<{
317
+ name: string;
318
+ ok: boolean;
319
+ message?: string;
320
+ }> = [];
321
+
322
+ checks.push({ name: "policy_version", ok: policy.version === 1 });
323
+ checks.push({
324
+ name: "jit_default_role_ids_present",
325
+ ok:
326
+ policy.provisioning.jit.mode !== "createUserAndMembership" ||
327
+ policy.provisioning.jit.defaultRoleIds.length > 0,
328
+ message:
329
+ policy.provisioning.jit.mode === "createUserAndMembership" &&
330
+ policy.provisioning.jit.defaultRoleIds.length === 0
331
+ ? "At least one default roleId is required when JIT membership provisioning is enabled."
332
+ : undefined,
333
+ });
334
+ checks.push({
335
+ name: "jit_default_role_ids_known",
336
+ ok: policy.provisioning.jit.defaultRoleIds.every(
337
+ (roleId) => config.authorization.roles[roleId] !== undefined,
338
+ ),
339
+ message: policy.provisioning.jit.defaultRoleIds.every(
340
+ (roleId) => config.authorization.roles[roleId] !== undefined,
341
+ )
342
+ ? undefined
343
+ : "JIT defaultRoleIds contains unknown roleIds.",
344
+ });
345
+ checks.push({
346
+ name: "scim_reuse_supported",
347
+ ok:
348
+ policy.provisioning.scimReuse.user === "externalId" ||
349
+ policy.provisioning.scimReuse.user === "none",
350
+ });
351
+
352
+ return checks;
353
+ };
354
+
355
+ const recordEnterpriseAuditEvent = async (
356
+ ctx: ComponentCtx,
357
+ data: {
358
+ enterpriseId: string;
359
+ groupId: string;
360
+ eventType: string;
361
+ actorType: "user" | "system" | "scim" | "api_key" | "webhook";
362
+ actorId?: string;
363
+ subjectType: string;
364
+ subjectId?: string;
365
+ ok: boolean;
366
+ requestId?: string;
367
+ ip?: string;
368
+ metadata?: Record<string, unknown>;
369
+ },
370
+ ) => {
371
+ const { ok, ...rest } = data;
372
+ return (await ctx.runMutation(
373
+ config.component.public.enterpriseAuditEventCreate,
374
+ {
375
+ ...rest,
376
+ status: ok ? "success" : "failure",
377
+ occurredAt: Date.now(),
378
+ },
379
+ )) as string;
380
+ };
381
+
382
+ const emitEnterpriseWebhookDeliveries = async (
383
+ ctx: ComponentCtx,
384
+ data: {
385
+ enterpriseId: string;
386
+ eventType: string;
387
+ payload: Record<string, unknown>;
388
+ auditEventId?: string;
389
+ },
390
+ ) => {
391
+ const endpoints = await ctx.runQuery(
392
+ config.component.public.enterpriseWebhookEndpointList,
393
+ { enterpriseId: data.enterpriseId },
394
+ );
395
+ for (const endpoint of endpoints) {
396
+ if (
397
+ endpoint.status !== "active" ||
398
+ !endpoint.subscriptions.includes(data.eventType)
399
+ ) {
400
+ continue;
401
+ }
402
+ await ctx.runMutation(
403
+ config.component.public.enterpriseWebhookDeliveryEnqueue,
404
+ {
405
+ enterpriseId: data.enterpriseId,
406
+ endpointId: endpoint._id,
407
+ auditEventId: data.auditEventId,
408
+ eventType: data.eventType,
409
+ payload: data.payload,
410
+ nextAttemptAt: Date.now(),
411
+ },
412
+ );
413
+ }
414
+ };
415
+
416
+ const getEnterpriseScimContext = async (
417
+ ctx: ComponentReadCtx,
418
+ request: Request,
419
+ ) => {
420
+ const authHeader = request.headers.get("Authorization");
421
+ if (!authHeader?.startsWith("Bearer ")) {
422
+ throw new AuthError("MISSING_BEARER_TOKEN").toConvexError();
423
+ }
424
+ const token = authHeader.slice(7);
425
+ const scimConfig = await ctx.runQuery(
426
+ config.component.public.enterpriseScimConfigGetByTokenHash,
427
+ { tokenHash: await sha256(token) },
428
+ );
429
+ if (!scimConfig || scimConfig.status !== "active") {
430
+ throw new AuthError(
431
+ "INVALID_API_KEY",
432
+ "Invalid SCIM token.",
433
+ ).toConvexError();
434
+ }
435
+ const parsedPath = parseScimPath(new URL(request.url).pathname);
436
+ if (parsedPath.enterpriseId !== scimConfig.enterpriseId) {
437
+ throw new AuthError(
438
+ "INVALID_API_KEY",
439
+ "SCIM token/tenant mismatch.",
440
+ ).toConvexError();
441
+ }
442
+ const enterprise = await ctx.runQuery(
443
+ config.component.public.enterpriseGet,
444
+ {
445
+ enterpriseId: scimConfig.enterpriseId,
446
+ },
447
+ );
448
+ if (enterprise === null) {
449
+ throw new AuthError(
450
+ "INVALID_PARAMETERS",
451
+ "Enterprise not found.",
452
+ ).toConvexError();
453
+ }
454
+ return { scimConfig, enterprise, parsedPath };
455
+ };
456
+
457
+ type ScimState = {
458
+ ctx: ComponentCtx;
459
+ request: Request;
460
+ url: URL;
461
+ parsedPath: ReturnType<typeof parseScimPath>;
462
+ enterprise: Awaited<
463
+ ReturnType<typeof getEnterpriseScimContext>
464
+ >["enterprise"];
465
+ scimConfig: Awaited<
466
+ ReturnType<typeof getEnterpriseScimContext>
467
+ >["scimConfig"];
468
+ policy: ReturnType<typeof normalizeEnterprisePolicy>;
469
+ recordScimEvent: (
470
+ eventType: string,
471
+ ok: boolean,
472
+ subjectType: string,
473
+ subjectId?: string,
474
+ metadata?: Record<string, unknown>,
475
+ ) => Promise<void>;
476
+ };
477
+
478
+ type ScimHandler = (state: ScimState) => Promise<Response>;
479
+
480
+ const SCIM_SCHEMAS = [
481
+ {
482
+ id: SCIM_USER_SCHEMA_ID,
483
+ name: "User",
484
+ description: "User Account",
485
+ attributes: [
486
+ { name: "userName", type: "string", required: true },
487
+ { name: "displayName", type: "string" },
488
+ { name: "active", type: "boolean" },
489
+ { name: "emails", type: "complex", multiValued: true },
490
+ ],
491
+ },
492
+ {
493
+ id: SCIM_GROUP_SCHEMA_ID,
494
+ name: "Group",
495
+ description: "Group",
496
+ attributes: [
497
+ { name: "displayName", type: "string", required: true },
498
+ { name: "members", type: "complex", multiValued: true },
499
+ ],
500
+ },
501
+ ] as const;
502
+
503
+ const SCIM_RESOURCE_TYPES = [
504
+ {
505
+ id: "User",
506
+ name: "User",
507
+ endpoint: "/Users",
508
+ schema: SCIM_USER_SCHEMA_ID,
509
+ },
510
+ {
511
+ id: "Group",
512
+ name: "Group",
513
+ endpoint: "/Groups",
514
+ schema: SCIM_GROUP_SCHEMA_ID,
515
+ },
516
+ ] as const;
517
+
518
+ const handleStaticScimCollection = <T extends { id?: string; name?: string }>(
519
+ items: readonly T[],
520
+ resourceId: string | undefined,
521
+ opts: { by: "id" | "name"; notFound: string },
522
+ ) => {
523
+ if (resourceId !== undefined) {
524
+ const item = items.find(
525
+ (entry) => entry[opts.by] === decodeURIComponent(resourceId),
526
+ );
527
+ return item ? scimJson(item) : scimError(404, "notFound", opts.notFound);
528
+ }
529
+ return scimJson({
530
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
531
+ Resources: items,
532
+ totalResults: items.length,
533
+ startIndex: 1,
534
+ itemsPerPage: items.length,
535
+ });
536
+ };
537
+
538
+ const filterScimCollection = <T>(
539
+ items: T[],
540
+ filter: ReturnType<typeof parseScimListRequest>["filter"],
541
+ filters: Record<string, (item: T, value: string) => boolean>,
542
+ ) => {
543
+ if (!filter) {
544
+ return items;
545
+ }
546
+ const predicate = filters[filter.attribute];
547
+ if (!predicate) {
548
+ throw new Error("Unsupported SCIM filter.");
549
+ }
550
+ return items.filter((item) => predicate(item, filter.value));
551
+ };
552
+
553
+ const paginateScimCollection = <T>(
554
+ items: T[],
555
+ listRequest: ReturnType<typeof parseScimListRequest>,
556
+ ) => {
557
+ const start = listRequest.startIndex - 1;
558
+ return items.slice(start, start + listRequest.count);
559
+ };
560
+
561
+ const requireScimResourceId = (
562
+ resourceId: string | undefined,
563
+ label: string,
564
+ ) => {
565
+ if (!resourceId) {
566
+ return scimError(400, "invalidPath", `${label} resource ID is required.`);
567
+ }
568
+ return null;
569
+ };
570
+
571
+ const readScimJson = async (request: Request) =>
572
+ (await request.json()) as Record<string, any>;
573
+
574
+ let auth: any;
575
+ auth = {
576
+ ...createCoreDomains({
577
+ config,
578
+ getAuth: () => auth,
579
+ callInvalidateSessions,
580
+ callCreateAccountFromCredentials,
581
+ callRetrieveAccountWithCredentials,
582
+ callModifyAccount,
583
+ getEnrichCtx: () => enrichCtx,
584
+ inviteTokenAlphabet: INVITE_TOKEN_ALPHABET,
585
+ inviteTokenLength: INVITE_TOKEN_LENGTH,
586
+ }),
587
+ /**
588
+ * SSO namespace — enterprise SSO connection management, domain, OIDC,
589
+ * SAML, SCIM, audit, and webhook helpers.
590
+ */
591
+ sso: createSsoDomain({
592
+ config,
593
+ getAuth: () => auth,
594
+ normalizeEnterprisePolicy,
595
+ normalizeDomain,
596
+ getEnterpriseSecret,
597
+ loadEnterpriseOrThrow,
598
+ validateEnterprisePolicy,
599
+ recordEnterpriseAuditEvent,
600
+ emitEnterpriseWebhookDeliveries,
601
+ enterpriseNotFoundError,
602
+ ENTERPRISE_OIDC_CLIENT_SECRET_KIND,
603
+ requireEnv,
604
+ generateRandomString,
605
+ INVITE_TOKEN_ALPHABET,
606
+ sha256,
607
+ encryptSecret,
608
+ upsertProtocolConfig,
609
+ parseSamlIdpMetadata,
610
+ createServiceProviderMetadata,
611
+ getSamlServiceProviderOptions,
612
+ getPublicOidcConfig,
613
+ withOidcSecretState,
614
+ getOidcConfig,
615
+ getEnterpriseOidcUrls,
616
+ enterpriseOidcProviderId,
617
+ getPolicyFromEnterprise,
618
+ patchEnterprisePolicy,
619
+ }),
620
+ // HTTP wiring stays local to the factory because it still depends on a
621
+ // dense mix of OAuth, SAML, SCIM, cookie, and response helpers.
622
+ http: {
623
+ /**
624
+ * Register core HTTP routes for JWT verification and OAuth sign-in.
625
+ *
626
+ * ```ts
627
+ * import { httpRouter } from "convex/server";
628
+ * import { auth } from "./auth";
629
+ *
630
+ * const http = httpRouter();
631
+ *
632
+ * auth.http.add(http);
633
+ *
634
+ * export default http;
635
+ * ```
636
+ *
637
+ * The following routes are handled always:
638
+ *
639
+ * - `/.well-known/openid-configuration`
640
+ * - `/.well-known/jwks.json`
641
+ *
642
+ * The following routes are handled if OAuth is configured:
643
+ *
644
+ * - `/api/auth/signin/*`
645
+ * - `/api/auth/callback/*`
646
+ *
647
+ * @param http your HTTP router
648
+ */
649
+ add: (http: HttpRouter) => {
650
+ addOpenIdRoutes(http, {
651
+ getIssuer: () => requireEnv("CONVEX_SITE_URL"),
652
+ getJwks: () => requireEnv("JWKS"),
653
+ });
654
+
655
+ if (hasSSO) {
656
+ const handleSamlAcs = async (
657
+ ctx: GenericActionCtx<any>,
658
+ request: Request,
659
+ runtimeRoute: SSORuntimeRoute,
660
+ ) =>
661
+ Fx.run(
662
+ Fx.gen(function* () {
663
+ yield* Fx.guard(
664
+ runtimeRoute.protocol !== "saml" ||
665
+ runtimeRoute.rest.length !== 1 ||
666
+ runtimeRoute.rest[0] !== "acs",
667
+ Fx.fail(
668
+ new AuthError(
669
+ "INVALID_PARAMETERS",
670
+ "Invalid enterprise runtime path.",
671
+ ).toConvexError(),
672
+ ),
673
+ );
674
+
675
+ const enterpriseId = runtimeRoute.enterpriseId;
676
+ const { loaded, enterprise, saml } = yield* Fx.from({
677
+ ok: () => loadActiveEnterpriseSamlOrThrow(ctx, enterpriseId),
678
+ err: (e) => e,
679
+ });
680
+
681
+ const parsedResponse = yield* Fx.from({
682
+ ok: () =>
683
+ parseEnterpriseSamlLoginResponse({
684
+ request,
685
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
686
+ source: { kind: "enterprise", id: enterprise._id },
687
+ config: loaded.config,
688
+ }),
689
+ err: (e) =>
690
+ new AuthError(
691
+ "OAUTH_PROVIDER_ERROR",
692
+ `SAML response parse failed: ${e instanceof Error ? e.message : String(e)}`,
693
+ ).toConvexError(),
694
+ });
695
+
696
+ yield* Fx.from({
697
+ ok: () => {
698
+ validateEnterpriseSamlLoginRelayState({
699
+ relayState: parsedResponse.relayState,
700
+ source: { kind: "enterprise", id: enterprise._id },
701
+ inResponseTo:
702
+ parsedResponse.parsed.extract?.response?.inResponseTo,
703
+ });
704
+ return Promise.resolve();
705
+ },
706
+ err: () =>
707
+ new AuthError(
708
+ "OAUTH_INVALID_STATE",
709
+ "SAML RelayState did not match the pending login request.",
710
+ ).toConvexError(),
711
+ });
712
+
713
+ const { samlAttributes, samlSessionIndex, ...userProfile } =
714
+ profileFromSamlExtract(
715
+ parsedResponse.parsed.extract,
716
+ saml.attributeMapping,
717
+ );
718
+ const profile = userProfile as Record<string, unknown> & {
719
+ id: string;
720
+ };
721
+
722
+ const maybeRedirectTo = useRedirectToParam(
723
+ enterpriseSamlProviderId(enterprise._id),
724
+ getCookies(request),
725
+ );
726
+
727
+ const verificationCode = yield* Fx.from({
728
+ ok: () =>
729
+ callUserOAuth(ctx, {
730
+ provider: enterpriseSamlProviderId(enterprise._id),
731
+ providerAccountId: profile.id,
732
+ profile,
733
+ signature: parsedResponse.relayState.signature,
734
+ accountExtend: {
735
+ identity: {
736
+ protocol: "saml",
737
+ enterpriseId: enterprise._id,
738
+ subject: profile.id,
739
+ entityId:
740
+ typeof saml.entityId === "string"
741
+ ? saml.entityId
742
+ : undefined,
743
+ },
744
+ saml: {
745
+ attributes: samlAttributes,
746
+ sessionIndex: samlSessionIndex,
747
+ },
748
+ },
749
+ }),
750
+ err: (e) => e,
751
+ });
752
+
753
+ const destinationUrl = yield* Fx.from({
754
+ ok: () =>
755
+ redirectAbsoluteUrl(config, {
756
+ redirectTo:
757
+ maybeRedirectTo?.redirectTo ??
758
+ (typeof parsedResponse.relayState.redirectTo ===
759
+ "string"
760
+ ? parsedResponse.relayState.redirectTo
761
+ : undefined),
762
+ }),
763
+ err: (e) => e,
764
+ });
765
+
766
+ const vurl = setURLSearchParam(
767
+ destinationUrl,
768
+ "code",
769
+ verificationCode,
770
+ );
771
+ const vheaders = new Headers({ Location: vurl });
772
+ vheaders.set("Cache-Control", "must-revalidate");
773
+ for (const { name, value, options } of maybeRedirectTo !== null
774
+ ? [maybeRedirectTo.updatedCookie]
775
+ : []) {
776
+ vheaders.append(
777
+ "Set-Cookie",
778
+ serializeCookie(name, value, options),
779
+ );
780
+ }
781
+ return new Response(null, { status: 302, headers: vheaders });
782
+ }).pipe(Fx.recover((e) => Fx.fatal(e))),
783
+ );
784
+
785
+ const handleSamlSlo = async (
786
+ ctx: GenericActionCtx<any>,
787
+ request: Request,
788
+ runtimeRoute: SSORuntimeRoute,
789
+ ) => {
790
+ if (
791
+ runtimeRoute.protocol !== "saml" ||
792
+ runtimeRoute.rest.length !== 1 ||
793
+ runtimeRoute.rest[0] !== "slo"
794
+ ) {
795
+ throw new AuthError(
796
+ "INVALID_PARAMETERS",
797
+ "Invalid enterprise runtime path.",
798
+ ).toConvexError();
799
+ }
800
+ const { loaded, enterprise } =
801
+ await loadActiveEnterpriseSamlOrThrow(
802
+ ctx,
803
+ runtimeRoute.enterpriseId,
804
+ );
805
+ const parsedMessage = await parseEnterpriseSamlLogoutMessage({
806
+ request,
807
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
808
+ source: { kind: "enterprise", id: enterprise._id },
809
+ config: loaded.config,
810
+ });
811
+ if (parsedMessage.hasSamlRequest && parsedMessage.parsedRequest) {
812
+ const responseContext = (
813
+ parsedMessage.runtime.sp as any
814
+ ).createLogoutResponse(
815
+ parsedMessage.runtime.idp as any,
816
+ parsedMessage.parsedRequest.extract,
817
+ parsedMessage.binding as any,
818
+ parsedMessage.relayState ?? "",
819
+ ) as any;
820
+ if (parsedMessage.binding === "redirect") {
821
+ return new Response(null, {
822
+ status: 302,
823
+ headers: { Location: responseContext.context },
824
+ });
825
+ }
826
+ return createSamlPostBindingResponse({
827
+ endpoint: responseContext.entityEndpoint,
828
+ parameter: "SAMLResponse",
829
+ value: responseContext.context,
830
+ relayState: parsedMessage.relayState,
831
+ });
832
+ }
833
+ if (parsedMessage.hasSamlResponse) {
834
+ return new Response(null, { status: 204 });
835
+ }
836
+ throw new AuthError(
837
+ "INVALID_PARAMETERS",
838
+ "Missing SAML logout payload.",
839
+ ).toConvexError();
840
+ };
841
+
842
+ const handleScimRequest = async (
843
+ ctx: GenericActionCtx<any>,
844
+ request: Request,
845
+ ) => {
846
+ try {
847
+ const { scimConfig, enterprise, parsedPath } =
848
+ await getEnterpriseScimContext(ctx, request);
849
+ const url = new URL(request.url);
850
+ const state: ScimState = {
851
+ ctx,
852
+ request,
853
+ url,
854
+ parsedPath,
855
+ enterprise,
856
+ scimConfig,
857
+ policy: getPolicyFromEnterprise(enterprise),
858
+ recordScimEvent: async (
859
+ eventType,
860
+ ok,
861
+ subjectType,
862
+ subjectId,
863
+ metadata,
864
+ ) => {
865
+ const auditEventId = await recordEnterpriseAuditEvent(ctx, {
866
+ enterpriseId: enterprise._id,
867
+ groupId: enterprise.groupId,
868
+ eventType,
869
+ actorType: "scim",
870
+ subjectType,
871
+ subjectId,
872
+ ok,
873
+ metadata,
874
+ });
875
+ await emitEnterpriseWebhookDeliveries(ctx, {
876
+ enterpriseId: enterprise._id,
877
+ eventType,
878
+ auditEventId,
879
+ payload: {
880
+ enterpriseId: enterprise._id,
881
+ subjectId,
882
+ metadata,
883
+ },
884
+ });
885
+ },
886
+ };
887
+
888
+ const handleUsersGet: ScimHandler = async (state) => {
889
+ const members = await auth.member.list(state.ctx, {
890
+ where: { groupId: state.enterprise.groupId },
891
+ limit: 100,
892
+ });
893
+ const identities = await state.ctx.runQuery(
894
+ config.component.public
895
+ .enterpriseScimIdentityListByEnterprise,
896
+ { enterpriseId: state.enterprise._id },
897
+ );
898
+ const identityByUserId = new Map(
899
+ identities
900
+ .filter((identity: any) => identity.userId !== undefined)
901
+ .map((identity: any) => [identity.userId, identity]),
902
+ );
903
+ const users = (
904
+ await Promise.all(
905
+ members.items.map(async (member: any) => {
906
+ const user = await auth.user.get(
907
+ state.ctx,
908
+ member.userId,
909
+ );
910
+ return user
911
+ ? {
912
+ user,
913
+ member,
914
+ identity: identityByUserId.get(user._id),
915
+ }
916
+ : null;
917
+ }),
918
+ )
919
+ ).filter(Boolean) as Array<{
920
+ user: any;
921
+ member: any;
922
+ identity?: any;
923
+ }>;
924
+ const listRequest = parseScimListRequest(state.url);
925
+ const filtered = filterScimCollection(
926
+ users,
927
+ listRequest.filter,
928
+ {
929
+ id: (item: { user: any }, value: string) =>
930
+ item.user._id === value,
931
+ externalId: (item: { identity?: any }, value: string) =>
932
+ item.identity?.externalId === value,
933
+ userName: (item: { user: any }, value: string) =>
934
+ item.user.email === value,
935
+ "emails.value": (item: { user: any }, value: string) =>
936
+ item.user.email === value,
937
+ active: (
938
+ item: { identity?: any; member: any },
939
+ value: string,
940
+ ) =>
941
+ String(
942
+ item.identity?.active ??
943
+ item.member.status === "active",
944
+ ) === value,
945
+ },
946
+ );
947
+ if (state.parsedPath.resourceId) {
948
+ const resource = filtered.find(
949
+ ({ user }) => user._id === state.parsedPath.resourceId,
950
+ );
951
+ return resource
952
+ ? scimJson(
953
+ serializeScimUser({
954
+ id: resource.user._id,
955
+ user: resource.user,
956
+ externalId: resource.identity?.externalId,
957
+ location: `${state.url.origin}${state.url.pathname.replace(/\/[^/]+$/, "")}/${resource.user._id}`,
958
+ active:
959
+ resource.identity?.active ??
960
+ resource.member.status === "active",
961
+ }),
962
+ 200,
963
+ {
964
+ Location: `${state.url.origin}${state.url.pathname.replace(/\/[^/]+$/, "")}/${resource.user._id}`,
965
+ },
966
+ )
967
+ : scimError(404, "notFound", "User not found.");
968
+ }
969
+ const paged = paginateScimCollection(filtered, listRequest);
970
+ await state.recordScimEvent(
971
+ "enterprise.scim.read",
972
+ true,
973
+ "enterprise_scim",
974
+ state.scimConfig._id,
975
+ );
976
+ return scimJson({
977
+ schemas: [
978
+ "urn:ietf:params:scim:api:messages:2.0:ListResponse",
979
+ ],
980
+ Resources: paged.map(({ user, identity, member }) =>
981
+ serializeScimUser({
982
+ id: user._id,
983
+ user,
984
+ externalId: identity?.externalId,
985
+ location: `${state.url.origin}${state.url.pathname}/${user._id}`,
986
+ active: identity?.active ?? member.status === "active",
987
+ }),
988
+ ),
989
+ totalResults: filtered.length,
990
+ startIndex: listRequest.startIndex,
991
+ itemsPerPage: paged.length,
992
+ });
993
+ };
994
+
995
+ const handleUsersPost: ScimHandler = async (state) => {
996
+ const body = await readScimJson(state.request);
997
+ const primaryEmail = Array.isArray(body.emails)
998
+ ? (body.emails.find((entry) => entry.primary === true)
999
+ ?.value ?? body.emails[0]?.value)
1000
+ : undefined;
1001
+ const phone = Array.isArray(body.phoneNumbers)
1002
+ ? body.phoneNumbers[0]?.value
1003
+ : undefined;
1004
+ const userId = (await state.ctx.runMutation(
1005
+ config.component.public.userInsert,
1006
+ {
1007
+ data: {
1008
+ name: body.displayName ?? body.name?.formatted,
1009
+ email: primaryEmail ?? body.userName,
1010
+ ...(typeof (primaryEmail ?? body.userName) === "string"
1011
+ ? { emailVerificationTime: Date.now() }
1012
+ : {}),
1013
+ phone,
1014
+ ...(typeof phone === "string"
1015
+ ? { phoneVerificationTime: Date.now() }
1016
+ : {}),
1017
+ },
1018
+ },
1019
+ )) as string;
1020
+ try {
1021
+ await auth.member.create(state.ctx, {
1022
+ groupId: state.enterprise.groupId,
1023
+ userId,
1024
+ roleIds: state.policy.provisioning.jit.defaultRoleIds,
1025
+ status: body.active === false ? "inactive" : "active",
1026
+ });
1027
+ } catch {}
1028
+ if (typeof body.externalId === "string") {
1029
+ await state.ctx.runMutation(
1030
+ config.component.public.enterpriseScimIdentityUpsert,
1031
+ {
1032
+ enterpriseId: state.enterprise._id,
1033
+ groupId: state.enterprise.groupId,
1034
+ resourceType: "user",
1035
+ externalId: body.externalId,
1036
+ userId,
1037
+ active: body.active !== false,
1038
+ raw: body,
1039
+ lastProvisionedAt: Date.now(),
1040
+ },
1041
+ );
1042
+ }
1043
+ await state.recordScimEvent(
1044
+ "enterprise.scim.user.created",
1045
+ true,
1046
+ "user",
1047
+ userId,
1048
+ );
1049
+ const createdUser = await auth.user.get(state.ctx, userId);
1050
+ const location = `${state.url.origin}${state.url.pathname}/${userId}`;
1051
+ return scimJson(
1052
+ serializeScimUser({
1053
+ id: userId,
1054
+ user: createdUser ?? {},
1055
+ externalId: body.externalId,
1056
+ location,
1057
+ active: body.active !== false,
1058
+ }),
1059
+ 201,
1060
+ { Location: location },
1061
+ );
1062
+ };
1063
+
1064
+ const handleUsersUpsert: ScimHandler = async (state) => {
1065
+ const missing = requireScimResourceId(
1066
+ state.parsedPath.resourceId,
1067
+ "User",
1068
+ );
1069
+ if (missing) return missing;
1070
+ const userId = state.parsedPath.resourceId!;
1071
+ const existingUser = await auth.user.get(state.ctx, userId);
1072
+ if (!existingUser) {
1073
+ return scimError(404, "notFound", "User not found.");
1074
+ }
1075
+ const body = await readScimJson(state.request);
1076
+ const patchData: Record<string, unknown> = {};
1077
+ let nextActive: boolean | undefined;
1078
+ if (state.request.method === "PUT") {
1079
+ patchData.name = body.displayName ?? body.name?.formatted;
1080
+ patchData.email =
1081
+ body.userName ??
1082
+ (Array.isArray(body.emails)
1083
+ ? body.emails[0]?.value
1084
+ : undefined);
1085
+ patchData.phone = Array.isArray(body.phoneNumbers)
1086
+ ? body.phoneNumbers[0]?.value
1087
+ : undefined;
1088
+ if (typeof patchData.email === "string") {
1089
+ patchData.emailVerificationTime = Date.now();
1090
+ }
1091
+ if (typeof patchData.phone === "string") {
1092
+ patchData.phoneVerificationTime = Date.now();
1093
+ }
1094
+ } else {
1095
+ for (const operation of Array.isArray(body.Operations)
1096
+ ? body.Operations
1097
+ : []) {
1098
+ if (operation.path === "active") {
1099
+ nextActive = operation.value;
1100
+ }
1101
+ if (
1102
+ operation.path === "displayName" ||
1103
+ operation.path === "name.formatted"
1104
+ ) {
1105
+ patchData.name = operation.value;
1106
+ }
1107
+ if (
1108
+ operation.path === "userName" ||
1109
+ operation.path === "emails.value"
1110
+ ) {
1111
+ patchData.email = operation.value;
1112
+ if (typeof operation.value === "string") {
1113
+ patchData.emailVerificationTime = Date.now();
1114
+ }
1115
+ }
1116
+ if (operation.path === "phoneNumbers.value") {
1117
+ patchData.phone = operation.value;
1118
+ if (typeof operation.value === "string") {
1119
+ patchData.phoneVerificationTime = Date.now();
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+ await state.ctx.runMutation(config.component.public.userPatch, {
1125
+ userId,
1126
+ data: patchData,
1127
+ });
1128
+ const membership = await auth.member.getByUserAndGroup(
1129
+ state.ctx,
1130
+ {
1131
+ groupId: state.enterprise.groupId,
1132
+ userId,
1133
+ },
1134
+ );
1135
+ if (membership) {
1136
+ await auth.member.update(state.ctx, membership._id, {
1137
+ status:
1138
+ body.active === false || nextActive === false
1139
+ ? "inactive"
1140
+ : "active",
1141
+ });
1142
+ }
1143
+ await state.ctx.runMutation(
1144
+ config.component.public.enterpriseScimIdentityUpsert,
1145
+ {
1146
+ enterpriseId: state.enterprise._id,
1147
+ groupId: state.enterprise.groupId,
1148
+ resourceType: "user",
1149
+ externalId:
1150
+ typeof body.externalId === "string"
1151
+ ? body.externalId
1152
+ : ((
1153
+ await state.ctx.runQuery(
1154
+ config.component.public
1155
+ .enterpriseScimIdentityGetByEnterpriseAndUser,
1156
+ {
1157
+ enterpriseId: state.enterprise._id,
1158
+ userId,
1159
+ },
1160
+ )
1161
+ )?.externalId ?? userId),
1162
+ userId,
1163
+ active: body.active !== false && nextActive !== false,
1164
+ raw: body,
1165
+ lastProvisionedAt: Date.now(),
1166
+ },
1167
+ );
1168
+ await state.recordScimEvent(
1169
+ "enterprise.scim.user.updated",
1170
+ true,
1171
+ "user",
1172
+ userId,
1173
+ );
1174
+ const updatedUser = await auth.user.get(state.ctx, userId);
1175
+ const location = `${state.url.origin}${state.url.pathname}`;
1176
+ return scimJson(
1177
+ serializeScimUser({
1178
+ id: userId,
1179
+ user: updatedUser ?? existingUser,
1180
+ externalId:
1181
+ typeof body.externalId === "string"
1182
+ ? body.externalId
1183
+ : undefined,
1184
+ location,
1185
+ active: body.active !== false && nextActive !== false,
1186
+ }),
1187
+ 200,
1188
+ { Location: location },
1189
+ );
1190
+ };
1191
+
1192
+ const handleUsersDelete: ScimHandler = async (state) => {
1193
+ const missing = requireScimResourceId(
1194
+ state.parsedPath.resourceId,
1195
+ "User",
1196
+ );
1197
+ if (missing) return missing;
1198
+ const userId = state.parsedPath.resourceId!;
1199
+ const membership = await auth.member.getByUserAndGroup(
1200
+ state.ctx,
1201
+ {
1202
+ groupId: state.enterprise.groupId,
1203
+ userId,
1204
+ },
1205
+ );
1206
+ if (membership) {
1207
+ await auth.member.delete(state.ctx, membership._id);
1208
+ }
1209
+ const identity = await state.ctx.runQuery(
1210
+ config.component.public
1211
+ .enterpriseScimIdentityGetByEnterpriseAndUser,
1212
+ {
1213
+ enterpriseId: state.enterprise._id,
1214
+ userId,
1215
+ },
1216
+ );
1217
+ if (identity) {
1218
+ if (state.policy.provisioning.deprovision.mode === "hard") {
1219
+ await state.ctx.runMutation(
1220
+ config.component.public.enterpriseScimIdentityDelete,
1221
+ { identityId: identity._id },
1222
+ );
1223
+ } else {
1224
+ await state.ctx.runMutation(
1225
+ config.component.public.enterpriseScimIdentityUpsert,
1226
+ {
1227
+ enterpriseId: identity.enterpriseId,
1228
+ groupId: identity.groupId,
1229
+ resourceType: identity.resourceType,
1230
+ externalId: identity.externalId,
1231
+ userId: identity.userId,
1232
+ mappedGroupId: identity.mappedGroupId,
1233
+ active: false,
1234
+ raw: identity.raw,
1235
+ lastProvisionedAt: Date.now(),
1236
+ },
1237
+ );
1238
+ }
1239
+ }
1240
+ await state.recordScimEvent(
1241
+ "enterprise.scim.user.deleted",
1242
+ true,
1243
+ "user",
1244
+ userId,
1245
+ );
1246
+ return new Response(null, { status: 204 });
1247
+ };
1248
+
1249
+ const handleGroupsGet: ScimHandler = async (state) => {
1250
+ const groupsList = await auth.group.list(state.ctx, {
1251
+ where: { parentGroupId: state.enterprise.groupId },
1252
+ limit: 100,
1253
+ });
1254
+ const identities = await state.ctx.runQuery(
1255
+ config.component.public
1256
+ .enterpriseScimIdentityListByEnterprise,
1257
+ { enterpriseId: state.enterprise._id },
1258
+ );
1259
+ const identityByGroupId = new Map(
1260
+ identities
1261
+ .filter(
1262
+ (identity: any) => identity.mappedGroupId !== undefined,
1263
+ )
1264
+ .map((identity: any) => [identity.mappedGroupId, identity]),
1265
+ );
1266
+ const groups = groupsList.items.map((group: any) => ({
1267
+ group,
1268
+ identity: identityByGroupId.get(group._id),
1269
+ }));
1270
+ const listRequest = parseScimListRequest(state.url);
1271
+ const filtered = filterScimCollection<{
1272
+ group: any;
1273
+ identity?: any;
1274
+ }>(groups, listRequest.filter, {
1275
+ id: (item: { group: any }, value: string) =>
1276
+ item.group._id === value,
1277
+ externalId: (item: { identity?: any }, value: string) =>
1278
+ item.identity?.externalId === value,
1279
+ displayName: (item: { group: any }, value: string) =>
1280
+ item.group.name === value,
1281
+ });
1282
+ if (state.parsedPath.resourceId) {
1283
+ const resource = filtered.find(
1284
+ ({ group }) => group._id === state.parsedPath.resourceId,
1285
+ );
1286
+ if (!resource) {
1287
+ return scimError(404, "notFound", "Group not found.");
1288
+ }
1289
+ const members = (
1290
+ await auth.member.list(state.ctx, {
1291
+ where: {
1292
+ groupId: resource.group._id,
1293
+ status: "active",
1294
+ },
1295
+ limit: 100,
1296
+ })
1297
+ ).items.map((member: any) => ({ value: member.userId }));
1298
+ const location = `${state.url.origin}${state.url.pathname.replace(/\/[^/]+$/, "")}/${resource.group._id}`;
1299
+ return scimJson(
1300
+ serializeScimGroup({
1301
+ id: resource.group._id,
1302
+ group: resource.group,
1303
+ externalId: resource.identity?.externalId,
1304
+ location,
1305
+ members,
1306
+ }),
1307
+ 200,
1308
+ { Location: location },
1309
+ );
1310
+ }
1311
+ const paged = paginateScimCollection(filtered, listRequest);
1312
+ return scimJson({
1313
+ schemas: [
1314
+ "urn:ietf:params:scim:api:messages:2.0:ListResponse",
1315
+ ],
1316
+ Resources: paged.map(({ group, identity }) =>
1317
+ serializeScimGroup({
1318
+ id: group._id,
1319
+ group,
1320
+ externalId: identity?.externalId,
1321
+ location: `${state.url.origin}${state.url.pathname}/${group._id}`,
1322
+ }),
1323
+ ),
1324
+ totalResults: filtered.length,
1325
+ startIndex: listRequest.startIndex,
1326
+ itemsPerPage: paged.length,
1327
+ });
1328
+ };
1329
+
1330
+ const handleGroupsPost: ScimHandler = async (state) => {
1331
+ const body = await readScimJson(state.request);
1332
+ const { groupId } = await auth.group.create(state.ctx, {
1333
+ name: String(body.displayName ?? "Group"),
1334
+ parentGroupId: state.enterprise.groupId,
1335
+ type: "organization",
1336
+ });
1337
+ await state.ctx.runMutation(
1338
+ config.component.public.enterpriseScimIdentityUpsert,
1339
+ {
1340
+ enterpriseId: state.enterprise._id,
1341
+ groupId: state.enterprise.groupId,
1342
+ resourceType: "group",
1343
+ externalId: body.externalId ?? groupId,
1344
+ mappedGroupId: groupId,
1345
+ active: true,
1346
+ raw: body,
1347
+ lastProvisionedAt: Date.now(),
1348
+ },
1349
+ );
1350
+ for (const member of Array.isArray(body.members)
1351
+ ? body.members
1352
+ : []) {
1353
+ try {
1354
+ await auth.member.create(state.ctx, {
1355
+ groupId,
1356
+ userId: String(member.value),
1357
+ roleIds: state.policy.provisioning.jit.defaultRoleIds,
1358
+ status: "active",
1359
+ });
1360
+ } catch {}
1361
+ }
1362
+ await state.recordScimEvent(
1363
+ "enterprise.scim.group.created",
1364
+ true,
1365
+ "group",
1366
+ groupId,
1367
+ );
1368
+ const group = await auth.group.get(state.ctx, groupId);
1369
+ const location = `${state.url.origin}${state.url.pathname}/${groupId}`;
1370
+ return scimJson(
1371
+ serializeScimGroup({
1372
+ id: groupId,
1373
+ group: group ?? {},
1374
+ externalId: body.externalId,
1375
+ location,
1376
+ members: (
1377
+ await auth.member.list(state.ctx, {
1378
+ where: { groupId, status: "active" },
1379
+ limit: 100,
1380
+ })
1381
+ ).items.map((member: any) => ({ value: member.userId })),
1382
+ }),
1383
+ 201,
1384
+ { Location: location },
1385
+ );
1386
+ };
1387
+
1388
+ const handleGroupsPatch: ScimHandler = async (state) => {
1389
+ const missing = requireScimResourceId(
1390
+ state.parsedPath.resourceId,
1391
+ "Group",
1392
+ );
1393
+ if (missing) return missing;
1394
+ const groupId = state.parsedPath.resourceId!;
1395
+ const body = await readScimJson(state.request);
1396
+ for (const operation of Array.isArray(body.Operations)
1397
+ ? body.Operations
1398
+ : []) {
1399
+ if (operation.path === "displayName") {
1400
+ await auth.group.update(state.ctx, groupId, {
1401
+ name: operation.value,
1402
+ });
1403
+ }
1404
+ if (operation.path === "members" && operation.op === "add") {
1405
+ for (const member of Array.isArray(operation.value)
1406
+ ? operation.value
1407
+ : []) {
1408
+ try {
1409
+ await auth.member.create(state.ctx, {
1410
+ groupId,
1411
+ userId: String(member.value),
1412
+ roleIds: state.policy.provisioning.jit.defaultRoleIds,
1413
+ status: "active",
1414
+ });
1415
+ } catch {}
1416
+ }
1417
+ }
1418
+ if (
1419
+ operation.path === "members" &&
1420
+ operation.op === "replace"
1421
+ ) {
1422
+ const currentMembers = (
1423
+ await auth.member.list(state.ctx, {
1424
+ where: { groupId, status: "active" },
1425
+ limit: 100,
1426
+ })
1427
+ ).items as Array<{ _id: string; userId: string }>;
1428
+ const currentUserIds = new Set<string>(
1429
+ currentMembers.map((member) => member.userId),
1430
+ );
1431
+ const nextUserIds = new Set<string>(
1432
+ (Array.isArray(operation.value)
1433
+ ? operation.value
1434
+ : []
1435
+ ).map((member: any) => String(member.value)),
1436
+ );
1437
+ for (const member of currentMembers) {
1438
+ if (!nextUserIds.has(member.userId)) {
1439
+ await auth.member.delete(state.ctx, member._id);
1440
+ }
1441
+ }
1442
+ for (const userId of nextUserIds.values()) {
1443
+ if (!currentUserIds.has(userId)) {
1444
+ try {
1445
+ await auth.member.create(state.ctx, {
1446
+ groupId,
1447
+ userId,
1448
+ roleIds:
1449
+ state.policy.provisioning.jit.defaultRoleIds,
1450
+ status: "active",
1451
+ });
1452
+ } catch {}
1453
+ }
1454
+ }
1455
+ }
1456
+ if (
1457
+ typeof operation.path === "string" &&
1458
+ operation.op === "remove" &&
1459
+ operation.path.startsWith("members[")
1460
+ ) {
1461
+ const match = operation.path.match(
1462
+ /^members\[value eq "([^"]+)"\]$/,
1463
+ );
1464
+ const userId = match?.[1];
1465
+ if (userId) {
1466
+ const membership = await auth.member.getByUserAndGroup(
1467
+ state.ctx,
1468
+ { groupId, userId },
1469
+ );
1470
+ if (membership) {
1471
+ await auth.member.delete(state.ctx, membership._id);
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ await state.recordScimEvent(
1477
+ "enterprise.scim.group.updated",
1478
+ true,
1479
+ "group",
1480
+ groupId,
1481
+ );
1482
+ const group = await auth.group.get(state.ctx, groupId);
1483
+ const location = `${state.url.origin}${state.url.pathname}`;
1484
+ const members = (
1485
+ await auth.member.list(state.ctx, {
1486
+ where: { groupId, status: "active" },
1487
+ limit: 100,
1488
+ })
1489
+ ).items as Array<{ userId: string }>;
1490
+ return scimJson(
1491
+ serializeScimGroup({
1492
+ id: groupId,
1493
+ group: group ?? {},
1494
+ location,
1495
+ members: members.map((member) => ({
1496
+ value: member.userId,
1497
+ })),
1498
+ }),
1499
+ 200,
1500
+ { Location: location },
1501
+ );
1502
+ };
1503
+
1504
+ const handleGroupsDelete: ScimHandler = async (state) => {
1505
+ const missing = requireScimResourceId(
1506
+ state.parsedPath.resourceId,
1507
+ "Group",
1508
+ );
1509
+ if (missing) return missing;
1510
+ const groupId = state.parsedPath.resourceId!;
1511
+ await auth.group.delete(state.ctx, groupId);
1512
+ const identity = await state.ctx.runQuery(
1513
+ config.component.public
1514
+ .enterpriseScimIdentityGetByMappedGroup,
1515
+ { mappedGroupId: groupId },
1516
+ );
1517
+ if (identity) {
1518
+ await state.ctx.runMutation(
1519
+ config.component.public.enterpriseScimIdentityDelete,
1520
+ { identityId: identity._id },
1521
+ );
1522
+ }
1523
+ await state.recordScimEvent(
1524
+ "enterprise.scim.group.deleted",
1525
+ true,
1526
+ "group",
1527
+ groupId,
1528
+ );
1529
+ return new Response(null, { status: 204 });
1530
+ };
1531
+
1532
+ const scimHandlers: Record<
1533
+ string,
1534
+ Partial<Record<string, ScimHandler>>
1535
+ > = {
1536
+ ServiceProviderConfig: {
1537
+ GET: async () =>
1538
+ scimJson({
1539
+ schemas: [
1540
+ "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",
1541
+ ],
1542
+ patch: { supported: true },
1543
+ bulk: {
1544
+ supported: false,
1545
+ maxOperations: 0,
1546
+ maxPayloadSize: 0,
1547
+ },
1548
+ filter: { supported: true, maxResults: 100 },
1549
+ changePassword: { supported: false },
1550
+ sort: { supported: false },
1551
+ etag: { supported: false },
1552
+ authenticationSchemes: [
1553
+ {
1554
+ type: "oauthbearertoken",
1555
+ name: "Bearer Token",
1556
+ description:
1557
+ "Use the SCIM token generated by Convex Auth enterprise.",
1558
+ },
1559
+ ],
1560
+ }),
1561
+ },
1562
+ Schemas: {
1563
+ GET: async (state) =>
1564
+ handleStaticScimCollection(
1565
+ SCIM_SCHEMAS,
1566
+ state.parsedPath.resourceId,
1567
+ {
1568
+ by: "id",
1569
+ notFound: "Schema not found.",
1570
+ },
1571
+ ),
1572
+ },
1573
+ ResourceTypes: {
1574
+ GET: async (state) =>
1575
+ handleStaticScimCollection(
1576
+ SCIM_RESOURCE_TYPES,
1577
+ state.parsedPath.resourceId,
1578
+ { by: "name", notFound: "Resource type not found." },
1579
+ ),
1580
+ },
1581
+ Users: {
1582
+ GET: handleUsersGet,
1583
+ POST: handleUsersPost,
1584
+ PATCH: handleUsersUpsert,
1585
+ PUT: handleUsersUpsert,
1586
+ DELETE: handleUsersDelete,
1587
+ },
1588
+ Groups: {
1589
+ GET: handleGroupsGet,
1590
+ POST: handleGroupsPost,
1591
+ PATCH: handleGroupsPatch,
1592
+ DELETE: handleGroupsDelete,
1593
+ },
1594
+ };
1595
+
1596
+ const handler =
1597
+ scimHandlers[state.parsedPath.resource]?.[state.request.method];
1598
+ return handler
1599
+ ? await handler(state)
1600
+ : scimError(404, "notFound", "SCIM resource not found.");
1601
+ } catch (error) {
1602
+ if (
1603
+ error instanceof Error &&
1604
+ error.message === "Unsupported SCIM filter."
1605
+ ) {
1606
+ return scimError(400, "invalidFilter", error.message);
1607
+ }
1608
+ if (isAuthError(error)) {
1609
+ const code = error.data.code as string;
1610
+ const status =
1611
+ code === "MISSING_BEARER_TOKEN" || code === "INVALID_API_KEY"
1612
+ ? 401
1613
+ : 400;
1614
+ return scimError(status, code, error.data.message);
1615
+ }
1616
+ throw error;
1617
+ }
1618
+ };
1619
+
1620
+ addSSORoutes(http, {
1621
+ routeBase: ENTERPRISE_CONTROL_ROUTE_BASE,
1622
+ convertErrorsToResponse,
1623
+ handleSamlMetadata: async (ctx, _request, runtimeRoute) => {
1624
+ const { loaded } = await loadActiveEnterpriseSamlOrThrow(
1625
+ ctx,
1626
+ runtimeRoute.enterpriseId,
1627
+ );
1628
+ return new Response(
1629
+ createEnterpriseSamlMetadataXml({
1630
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
1631
+ source: loaded.source,
1632
+ config: loaded.config,
1633
+ }),
1634
+ {
1635
+ status: 200,
1636
+ headers: { "Content-Type": "application/xml" },
1637
+ },
1638
+ );
1639
+ },
1640
+ handleSamlSignIn: async (ctx, request, runtimeRoute) => {
1641
+ const url = new URL(request.url);
1642
+ const verifier = url.searchParams.get("code");
1643
+ if (!verifier) {
1644
+ throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
1645
+ }
1646
+ const { loaded, enterprise } =
1647
+ await loadActiveEnterpriseSamlOrThrow(
1648
+ ctx,
1649
+ runtimeRoute.enterpriseId,
1650
+ );
1651
+ const state = generateRandomString(24, INVITE_TOKEN_ALPHABET);
1652
+ const signInRequest = createEnterpriseSamlSignInRequest({
1653
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
1654
+ source: { kind: "enterprise", id: enterprise._id },
1655
+ config: loaded.config,
1656
+ state,
1657
+ signature: `saml ${enterprise._id} pending ${state}`,
1658
+ redirectTo: url.searchParams.get("redirectTo") ?? undefined,
1659
+ });
1660
+ const signature = `saml ${enterprise._id} ${signInRequest.requestId} ${state}`;
1661
+ await callVerifierSignature(ctx, { verifier, signature });
1662
+ const redirectTo = url.searchParams.get("redirectTo");
1663
+ const redirectCookies =
1664
+ redirectTo !== null
1665
+ ? [
1666
+ redirectToParamCookie(
1667
+ enterpriseSamlProviderId(enterprise._id),
1668
+ redirectTo,
1669
+ ),
1670
+ ]
1671
+ : [];
1672
+ const relayState = encodeEnterpriseSamlRelayState({
1673
+ source: { kind: "enterprise", id: enterprise._id },
1674
+ signature,
1675
+ requestId: signInRequest.requestId,
1676
+ state,
1677
+ redirectTo: url.searchParams.get("redirectTo") ?? undefined,
1678
+ });
1679
+ if (
1680
+ signInRequest.binding === "redirect" &&
1681
+ signInRequest.redirectUrl
1682
+ ) {
1683
+ const redirectUrl = new URL(signInRequest.redirectUrl);
1684
+ redirectUrl.searchParams.set("RelayState", relayState);
1685
+ const headers = new Headers({
1686
+ Location: redirectUrl.toString(),
1687
+ });
1688
+ for (const { name, value, options } of redirectCookies as any) {
1689
+ headers.append(
1690
+ "Set-Cookie",
1691
+ serializeCookie(name, value, options),
1692
+ );
1693
+ }
1694
+ return new Response(null, { status: 302, headers });
1695
+ }
1696
+ const response = createSamlPostBindingResponse({
1697
+ endpoint: signInRequest.post!.endpoint,
1698
+ parameter: "SAMLRequest",
1699
+ value: signInRequest.post!.value,
1700
+ relayState,
1701
+ });
1702
+ for (const { name, value, options } of redirectCookies as any) {
1703
+ response.headers.append(
1704
+ "Set-Cookie",
1705
+ serializeCookie(name, value, options),
1706
+ );
1707
+ }
1708
+ return response;
1709
+ },
1710
+ handleOidcSignIn: async (ctx, request, runtimeRoute) => {
1711
+ const url = new URL(request.url);
1712
+ const verifier = url.searchParams.get("code");
1713
+ if (!verifier) {
1714
+ throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
1715
+ }
1716
+ const { enterprise, oidc } = await loadEnterpriseOidcOrThrow(
1717
+ ctx,
1718
+ runtimeRoute.enterpriseId,
1719
+ );
1720
+ const { providerId, provider, oauthConfig } =
1721
+ await createEnterpriseOidcRuntime({
1722
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
1723
+ enterpriseId: enterprise._id,
1724
+ oidc,
1725
+ });
1726
+ const { redirect, cookies, signature } =
1727
+ await createOAuthAuthorizationURL(
1728
+ providerId,
1729
+ provider,
1730
+ oauthConfig,
1731
+ );
1732
+ await callVerifierSignature(ctx, { verifier, signature });
1733
+ const redirectTo = url.searchParams.get("redirectTo");
1734
+ const headers_ = new Headers({ Location: redirect });
1735
+ for (const { name, value, options } of [
1736
+ ...cookies,
1737
+ ...(redirectTo !== null
1738
+ ? [redirectToParamCookie(providerId, redirectTo)]
1739
+ : []),
1740
+ ] as any) {
1741
+ headers_.append(
1742
+ "Set-Cookie",
1743
+ serializeCookie(name, value, options),
1744
+ );
1745
+ }
1746
+ return new Response(null, {
1747
+ status: 302,
1748
+ headers: headers_,
1749
+ });
1750
+ },
1751
+ handleOidcCallback: async (ctx, request, runtimeRoute) => {
1752
+ const url = new URL(request.url);
1753
+ const { enterprise, oidc } = await loadEnterpriseOidcOrThrow(
1754
+ ctx,
1755
+ runtimeRoute.enterpriseId,
1756
+ );
1757
+ const { providerId, provider, oauthConfig } =
1758
+ await createEnterpriseOidcRuntime({
1759
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
1760
+ enterpriseId: enterprise._id,
1761
+ oidc,
1762
+ });
1763
+ const cookies = getCookies(request);
1764
+ const maybeRedirectTo = useRedirectToParam(providerId, cookies);
1765
+ const destinationUrl = await redirectAbsoluteUrl(config, {
1766
+ redirectTo: maybeRedirectTo?.redirectTo,
1767
+ });
1768
+ const params = url.searchParams;
1769
+ const result = await Fx.run(
1770
+ handleOAuthCallback(
1771
+ providerId,
1772
+ provider,
1773
+ oauthConfig,
1774
+ Object.fromEntries(params.entries()),
1775
+ cookies,
1776
+ ),
1777
+ );
1778
+ const extraFields = oidc.extraFields as
1779
+ | Record<string, string>
1780
+ | undefined;
1781
+ let profile = result.profile as Record<string, unknown>;
1782
+ if (extraFields && typeof profile === "object" && profile) {
1783
+ const extend: Record<string, unknown> = {};
1784
+ for (const [claimName, fieldName] of Object.entries(
1785
+ extraFields,
1786
+ )) {
1787
+ if (claimName in profile) {
1788
+ extend[fieldName] = profile[claimName];
1789
+ }
1790
+ }
1791
+ if (Object.keys(extend).length > 0) {
1792
+ profile = { ...profile, extend };
1793
+ }
1794
+ }
1795
+
1796
+ const verificationCode = await callUserOAuth(ctx, {
1797
+ provider: providerId,
1798
+ providerAccountId: result.providerAccountId,
1799
+ profile,
1800
+ signature: result.signature,
1801
+ accountExtend: {
1802
+ identity: {
1803
+ protocol: "oidc",
1804
+ enterpriseId: enterprise._id,
1805
+ subject: result.providerAccountId,
1806
+ issuer:
1807
+ typeof oidc.issuer === "string" ? oidc.issuer : undefined,
1808
+ discoveryUrl:
1809
+ typeof oidc.discoveryUrl === "string"
1810
+ ? oidc.discoveryUrl
1811
+ : undefined,
1812
+ },
1813
+ },
1814
+ });
1815
+ const headers = new Headers({
1816
+ Location: setURLSearchParam(
1817
+ destinationUrl,
1818
+ "code",
1819
+ verificationCode,
1820
+ ),
1821
+ });
1822
+ for (const { name, value, options } of result.cookies) {
1823
+ headers.append(
1824
+ "Set-Cookie",
1825
+ serializeCookie(name, value, options as any),
1826
+ );
1827
+ }
1828
+ if (maybeRedirectTo) {
1829
+ headers.append(
1830
+ "Set-Cookie",
1831
+ serializeCookie(
1832
+ maybeRedirectTo.updatedCookie.name,
1833
+ maybeRedirectTo.updatedCookie.value,
1834
+ maybeRedirectTo.updatedCookie.options as any,
1835
+ ),
1836
+ );
1837
+ }
1838
+ return new Response(null, { status: 302, headers });
1839
+ },
1840
+ handleSamlAcs,
1841
+ handleSamlSlo,
1842
+ handleScimRequest,
1843
+ scimError,
1844
+ });
1845
+ } // end if (hasSSO)
1846
+
1847
+ if (hasOAuth) {
1848
+ addAuthRoutes(http, {
1849
+ handleSignIn: convertErrorsToResponse(400, async (ctx, request) => {
1850
+ const url = new URL(request.url);
1851
+ const pathParts = url.pathname.split("/");
1852
+ const providerId = pathParts.at(-1)!;
1853
+ if (providerId === null) {
1854
+ throw new AuthError("OAUTH_MISSING_PROVIDER").toConvexError();
1855
+ }
1856
+ const verifier = url.searchParams.get("code");
1857
+ if (verifier === null) {
1858
+ throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
1859
+ }
1860
+ const provider = getProviderOrThrow(providerId);
1861
+
1862
+ const oauthConfig = provider as OAuthMaterializedConfig;
1863
+ const { redirect, cookies, signature } =
1864
+ await createOAuthAuthorizationURL(
1865
+ providerId,
1866
+ oauthConfig.provider,
1867
+ oauthConfig,
1868
+ );
1869
+
1870
+ await callVerifierSignature(ctx, {
1871
+ verifier,
1872
+ signature,
1873
+ });
1874
+
1875
+ const redirectTo = url.searchParams.get("redirectTo");
1876
+ if (redirectTo !== null) {
1877
+ cookies.push(redirectToParamCookie(providerId, redirectTo));
1878
+ }
1879
+
1880
+ const headers = new Headers({ Location: redirect });
1881
+ for (const { name, value, options } of cookies) {
1882
+ headers.append(
1883
+ "Set-Cookie",
1884
+ serializeCookie(name, value, options as any),
1885
+ );
1886
+ }
1887
+
1888
+ return new Response(null, { status: 302, headers });
1889
+ }),
1890
+ handleCallback: async (ctx, request) => {
1891
+ const url = new URL(request.url);
1892
+ const providerId = new URL(request.url).pathname
1893
+ .split("/")
1894
+ .at(-1);
1895
+ if (!providerId) {
1896
+ throw new AuthError("OAUTH_MISSING_PROVIDER").toConvexError();
1897
+ }
1898
+ logWithLevel(
1899
+ LOG_LEVELS.DEBUG,
1900
+ "Handling OAuth callback for provider:",
1901
+ providerId,
1902
+ );
1903
+ const provider = getProviderOrThrow(providerId);
1904
+
1905
+ const cookies = getCookies(request);
1906
+
1907
+ const maybeRedirectTo = useRedirectToParam(provider.id, cookies);
1908
+
1909
+ const destinationUrl = await redirectAbsoluteUrl(config, {
1910
+ redirectTo: maybeRedirectTo?.redirectTo,
1911
+ });
1912
+
1913
+ const params = url.searchParams;
1914
+
1915
+ if (
1916
+ request.headers.get("Content-Type") ===
1917
+ "application/x-www-form-urlencoded"
1918
+ ) {
1919
+ const formData = await request.formData();
1920
+ formData.forEach((value, key) => {
1921
+ if (typeof value === "string") {
1922
+ params.append(key, value);
1923
+ }
1924
+ });
1925
+ }
1926
+
1927
+ return Fx.run(
1928
+ Fx.from({
1929
+ ok: async () => {
1930
+ const oauthConfig = provider as OAuthMaterializedConfig;
1931
+ const result = await Fx.run(
1932
+ handleOAuthCallback(
1933
+ providerId,
1934
+ oauthConfig.provider,
1935
+ oauthConfig,
1936
+ Object.fromEntries(params.entries()),
1937
+ cookies,
1938
+ ),
1939
+ );
1940
+ const oauthCookies = result.cookies;
1941
+ const { id: profileId, ...profileData } = result.profile;
1942
+ const { signature } = result;
1943
+
1944
+ const verificationCode = await callUserOAuth(ctx, {
1945
+ provider: providerId,
1946
+ providerAccountId: profileId,
1947
+ profile: profileData,
1948
+ signature,
1949
+ });
1950
+
1951
+ const redirUrl = setURLSearchParam(
1952
+ destinationUrl,
1953
+ "code",
1954
+ verificationCode,
1955
+ );
1956
+ const redirHeaders = new Headers({ Location: redirUrl });
1957
+ redirHeaders.set("Cache-Control", "must-revalidate");
1958
+ for (const { name, value, options } of [
1959
+ ...oauthCookies,
1960
+ ...(maybeRedirectTo !== null
1961
+ ? [maybeRedirectTo.updatedCookie]
1962
+ : []),
1963
+ ] as any) {
1964
+ redirHeaders.append(
1965
+ "Set-Cookie",
1966
+ serializeCookie(name, value, options),
1967
+ );
1968
+ }
1969
+ return new Response(null, {
1970
+ status: 302,
1971
+ headers: redirHeaders,
1972
+ });
1973
+ },
1974
+ err: (error) => error,
1975
+ }).pipe(
1976
+ Fx.recover((error) => {
1977
+ logError(error);
1978
+ const respHeaders = new Headers({
1979
+ Location: destinationUrl,
1980
+ });
1981
+ for (const { name, value, options } of maybeRedirectTo !==
1982
+ null
1983
+ ? [maybeRedirectTo.updatedCookie]
1984
+ : []) {
1985
+ respHeaders.append(
1986
+ "Set-Cookie",
1987
+ serializeCookie(name, value, options),
1988
+ );
1989
+ }
1990
+ return Fx.succeed(
1991
+ new Response(null, {
1992
+ status: 302,
1993
+ headers: respHeaders,
1994
+ }),
1995
+ );
1996
+ }),
1997
+ ),
1998
+ );
1999
+ },
2000
+ });
2001
+ }
2002
+ },
2003
+
2004
+ /**
2005
+ * Wrap an HTTP action handler with Bearer token authentication.
2006
+ *
2007
+ * Extracts the `Authorization: Bearer <key>` header, verifies the
2008
+ * API key via `auth.key.verify()`, and injects `ctx.key` with the
2009
+ * verified key info. Returns structured JSON error responses for
2010
+ * missing/invalid/revoked/expired/rate-limited keys.
2011
+ *
2012
+ * If the handler returns a plain object, it is auto-wrapped in a
2013
+ * `200 JSON` response. If it returns a `Response`, CORS headers
2014
+ * are merged and the response is passed through.
2015
+ *
2016
+ * ```ts
2017
+ * const handler = auth.http.action(async (ctx, request) => {
2018
+ * const data = await ctx.runQuery(api.data.get, { userId: ctx.key.userId });
2019
+ * return { data };
2020
+ * });
2021
+ * http.route({ path: "/api/data", method: "GET", handler });
2022
+ * ```
2023
+ *
2024
+ * @param handler - Receives enriched `ctx` (with `ctx.key`) and the raw `Request`.
2025
+ * @param options.scope - Optional scope check; returns 403 if the key lacks permission.
2026
+ * @param options.cors - CORS config; defaults to permissive (`*`).
2027
+ */
2028
+ action: createHttpAction(auth),
2029
+
2030
+ /**
2031
+ * Register a Bearer-authenticated route **and** its OPTIONS preflight
2032
+ * in a single call.
2033
+ *
2034
+ * ```ts
2035
+ * auth.http.route(http, {
2036
+ * path: "/api/messages",
2037
+ * method: "POST",
2038
+ * handler: async (ctx, request) => {
2039
+ * const { body } = await request.json();
2040
+ * await ctx.runMutation(internal.messages.sendAsUser, {
2041
+ * userId: ctx.key.userId,
2042
+ * body,
2043
+ * });
2044
+ * return { success: true };
2045
+ * },
2046
+ * });
2047
+ * ```
2048
+ *
2049
+ * @param http - The Convex HTTP router.
2050
+ * @param routeConfig.path - The URL path to match.
2051
+ * @param routeConfig.method - HTTP method (GET, POST, PUT, PATCH, DELETE).
2052
+ * @param routeConfig.handler - Receives enriched `ctx` (with `ctx.key`) and the raw `Request`.
2053
+ * @param routeConfig.scope - Optional scope check; returns 403 if the key lacks permission.
2054
+ * @param routeConfig.cors - CORS config; defaults to permissive (`*`).
2055
+ */
2056
+ route: createHttpRoute(createHttpAction(auth)),
2057
+ },
2058
+ };
2059
+
2060
+ const enrichCtx = <DataModel extends GenericDataModel>(
2061
+ ctx: GenericActionCtx<DataModel>,
2062
+ ) => ({
2063
+ ...ctx,
2064
+ auth: {
2065
+ ...ctx.auth,
2066
+ config,
2067
+ account: auth.account,
2068
+ session: auth.session,
2069
+ access: auth.access,
2070
+ provider: auth.provider,
2071
+ },
2072
+ });
2073
+
2074
+ return {
2075
+ /**
2076
+ * Helper for configuring HTTP actions.
2077
+ */
2078
+ auth,
2079
+ /**
2080
+ * Action called by the client to sign the user in.
2081
+ *
2082
+ * Also used for refreshing the session.
2083
+ */
2084
+ signIn: actionGeneric({
2085
+ args: {
2086
+ provider: v.optional(v.string()),
2087
+ params: v.optional(v.any()),
2088
+ verifier: v.optional(v.string()),
2089
+ refreshToken: v.optional(v.string()),
2090
+ calledBy: v.optional(v.string()),
2091
+ },
2092
+ handler: async (ctx, args): Promise<SignInActionResult> => {
2093
+ if (args.calledBy !== undefined) {
2094
+ logWithLevel("INFO", `\`auth:signIn\` called by ${args.calledBy}`);
2095
+ }
2096
+ const provider =
2097
+ args.provider !== undefined
2098
+ ? getProviderOrThrow(args.provider)
2099
+ : null;
2100
+ const result = await signInImpl(enrichCtx(ctx), provider, args, {
2101
+ generateTokens: true,
2102
+ allowExtraProviders: false,
2103
+ });
2104
+ return Fx.run(
2105
+ Fx.match(result, result.kind, {
2106
+ redirect: (r) =>
2107
+ Fx.succeed({
2108
+ kind: "redirect" as const,
2109
+ redirect: r.redirect,
2110
+ verifier: r.verifier,
2111
+ }),
2112
+ signedIn: (r) =>
2113
+ Fx.succeed({
2114
+ kind: "signedIn" as const,
2115
+ tokens: r.signedIn?.tokens ?? null,
2116
+ }),
2117
+ refreshTokens: (r) =>
2118
+ Fx.succeed({
2119
+ kind: "signedIn" as const,
2120
+ tokens: r.signedIn?.tokens ?? null,
2121
+ }),
2122
+ started: () => Fx.succeed({ kind: "started" as const }),
2123
+ passkeyOptions: (r) =>
2124
+ Fx.succeed({
2125
+ kind: "passkeyOptions" as const,
2126
+ options: r.options,
2127
+ verifier: r.verifier,
2128
+ }),
2129
+ totpRequired: (r) =>
2130
+ Fx.succeed({
2131
+ kind: "totpRequired" as const,
2132
+ verifier: r.verifier,
2133
+ }),
2134
+ totpSetup: (r) =>
2135
+ Fx.succeed({
2136
+ kind: "totpSetup" as const,
2137
+ totpSetup: {
2138
+ uri: r.uri,
2139
+ secret: r.secret,
2140
+ totpId: r.totpId,
2141
+ },
2142
+ verifier: r.verifier,
2143
+ }),
2144
+ deviceCode: (r) =>
2145
+ Fx.succeed({
2146
+ kind: "deviceCode" as const,
2147
+ deviceCode: {
2148
+ deviceCode: r.deviceCode,
2149
+ userCode: r.userCode,
2150
+ verificationUri: r.verificationUri,
2151
+ verificationUriComplete: r.verificationUriComplete,
2152
+ expiresIn: r.expiresIn,
2153
+ interval: r.interval,
2154
+ },
2155
+ }),
2156
+ }),
2157
+ );
2158
+ },
2159
+ }),
2160
+ /**
2161
+ * Action called by the client to invalidate the current session.
2162
+ */
2163
+ signOut: actionGeneric({
2164
+ args: {},
2165
+ handler: async (ctx) => {
2166
+ await callSignOut(ctx);
2167
+ },
2168
+ }),
2169
+
2170
+ /**
2171
+ * Internal mutation used by the library to read and write
2172
+ * to the database during signin and signout.
2173
+ */
2174
+ store: internalMutationGeneric({
2175
+ args: storeArgs,
2176
+ handler: async (ctx: MutationCtx, args) => {
2177
+ return storeImpl(ctx, args, getProviderOrThrow, config);
2178
+ },
2179
+ }),
2180
+ };
2181
+ }