@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
@@ -1,2365 +0,0 @@
1
- import { isAuthError } from "./errors.js";
2
- import { AuthError, Fx } from "./fx.js";
3
- import { LOG_LEVELS, TOKEN_SUB_CLAIM_DIVIDER, generateRandomString, logError, logWithLevel, requireEnv, sha256 } from "./utils.js";
4
- import { redirectToParamCookie, useRedirectToParam } from "./cookies.js";
5
- import { buildScopeChecker, checkKeyRateLimit, generateApiKey, hashApiKey } from "./keys.js";
6
- import { callModifyAccount } from "./mutations/account.js";
7
- import { callInvalidateSessions } from "./mutations/invalidate.js";
8
- import { SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID, createEnterpriseOidcRuntime, createEnterpriseSamlMetadataXml, createEnterpriseSamlSignInRequest, createSamlPostBindingResponse, createServiceProviderMetadata, encodeEnterpriseSamlRelayState, enterpriseOidcProviderId, enterpriseSamlProviderId, getEnterpriseOidcUrls, getOidcConfig, getSamlConfig, getSamlServiceProviderOptions, isEnterpriseSamlSourceActive, normalizeDomain, parseEnterpriseSamlLoginResponse, parseEnterpriseSamlLogoutMessage, parseSamlIdpMetadata, parseScimListRequest, parseScimPath, profileFromSamlExtract, scimError, scimJson, serializeScimGroup, serializeScimUser, upsertProtocolConfig, validateEnterpriseSamlLoginRelayState } from "./sso.js";
9
- import { callUserOAuth } from "./mutations/oauth.js";
10
- import { callCreateAccountFromCredentials } from "./mutations/register.js";
11
- import { callRetrieveAccountWithCredentials } from "./mutations/retrieve.js";
12
- import { callVerifierSignature } from "./mutations/signature.js";
13
- import { callSignOut } from "./mutations/signout.js";
14
- import { storeArgs, storeImpl } from "./mutations/index.js";
15
- import { createOAuthAuthorizationURL, handleOAuthCallback } from "./oauth.js";
16
- import { configDefaults, listAvailableProviders, materializeProvider } from "./providers.js";
17
- import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
18
- import { signInImpl } from "./signin.js";
19
- import { actionGeneric, httpActionGeneric, internalMutationGeneric } from "convex/server";
20
- import { ConvexError, v } from "convex/values";
21
- import { parse, serialize } from "cookie";
22
-
23
- //#region src/server/implementation.ts
24
- /**
25
- * Configure the Convex Auth library. Returns an object with
26
- * functions and `auth` helper. You must export the functions
27
- * from `convex/auth.ts` to make them callable:
28
- *
29
- * ```ts filename="convex/auth.ts"
30
- * import { createAuth } from "@robelest/convex-auth/component";
31
- * import { components } from "./_generated/api";
32
- *
33
- * export const auth = createAuth(components.auth, {
34
- * providers: [],
35
- * });
36
- * export const { signIn, signOut, store } = auth;
37
- * ```
38
- *
39
- * @returns An object with fields you should reexport from your
40
- * `convex/auth.ts` file.
41
- */
42
- function Auth(config_) {
43
- const config = configDefaults(config_);
44
- const hasOAuth = config.providers.some((provider) => provider.type === "oauth");
45
- const hasSSO = config.providers.some((provider) => provider.type === "sso");
46
- const getProviderOrThrow = (id, allowExtraProviders = false) => {
47
- const provider = config.providers.find((configuredProvider) => configuredProvider.id === id) ?? (allowExtraProviders ? config.extraProviders.find((configuredProvider) => configuredProvider.id === id) : void 0);
48
- if (provider === void 0) {
49
- const detail = `Provider \`${id}\` is not configured, available providers are ${listAvailableProviders(config, allowExtraProviders)}.`;
50
- logWithLevel(LOG_LEVELS.ERROR, detail);
51
- throw new AuthError("PROVIDER_NOT_CONFIGURED", detail, { provider: id }).toConvexError();
52
- }
53
- return provider;
54
- };
55
- const INVITE_TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
56
- const INVITE_TOKEN_LENGTH = 48;
57
- const enterpriseNotFoundError = "Enterprise not found.";
58
- const ENTERPRISE_CONTROL_ROUTE_BASE = "/api/auth/sso";
59
- const recordEnterpriseAuditEvent = async (ctx, data) => {
60
- const { ok, ...rest } = data;
61
- return await ctx.runMutation(config.component.public.enterpriseAuditEventCreate, {
62
- ...rest,
63
- status: ok ? "success" : "failure",
64
- occurredAt: Date.now()
65
- });
66
- };
67
- const emitEnterpriseWebhookDeliveries = async (ctx, data) => {
68
- const endpoints = await ctx.runQuery(config.component.public.enterpriseWebhookEndpointList, { enterpriseId: data.enterpriseId });
69
- for (const endpoint of endpoints) {
70
- if (endpoint.status !== "active" || !endpoint.subscriptions.includes(data.eventType)) continue;
71
- await ctx.runMutation(config.component.public.enterpriseWebhookDeliveryEnqueue, {
72
- enterpriseId: data.enterpriseId,
73
- endpointId: endpoint._id,
74
- auditEventId: data.auditEventId,
75
- eventType: data.eventType,
76
- payload: data.payload,
77
- nextAttemptAt: Date.now()
78
- });
79
- }
80
- };
81
- const getEnterpriseScimContext = async (ctx, request) => {
82
- const authHeader = request.headers.get("Authorization");
83
- if (!authHeader?.startsWith("Bearer ")) throw new AuthError("MISSING_BEARER_TOKEN").toConvexError();
84
- const token = authHeader.slice(7);
85
- const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByTokenHash, { tokenHash: await sha256(token) });
86
- if (!scimConfig || scimConfig.status !== "active") throw new AuthError("INVALID_API_KEY", "Invalid SCIM token.").toConvexError();
87
- const parsedPath = parseScimPath(new URL(request.url).pathname);
88
- if (parsedPath.enterpriseId !== scimConfig.enterpriseId) throw new AuthError("INVALID_API_KEY", "SCIM token/tenant mismatch.").toConvexError();
89
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: scimConfig.enterpriseId });
90
- if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
91
- return {
92
- scimConfig,
93
- enterprise,
94
- parsedPath
95
- };
96
- };
97
- const SCIM_SCHEMAS = [{
98
- id: SCIM_USER_SCHEMA_ID,
99
- name: "User",
100
- description: "User Account",
101
- attributes: [
102
- {
103
- name: "userName",
104
- type: "string",
105
- required: true
106
- },
107
- {
108
- name: "displayName",
109
- type: "string"
110
- },
111
- {
112
- name: "active",
113
- type: "boolean"
114
- },
115
- {
116
- name: "emails",
117
- type: "complex",
118
- multiValued: true
119
- }
120
- ]
121
- }, {
122
- id: SCIM_GROUP_SCHEMA_ID,
123
- name: "Group",
124
- description: "Group",
125
- attributes: [{
126
- name: "displayName",
127
- type: "string",
128
- required: true
129
- }, {
130
- name: "members",
131
- type: "complex",
132
- multiValued: true
133
- }]
134
- }];
135
- const SCIM_RESOURCE_TYPES = [{
136
- id: "User",
137
- name: "User",
138
- endpoint: "/Users",
139
- schema: SCIM_USER_SCHEMA_ID
140
- }, {
141
- id: "Group",
142
- name: "Group",
143
- endpoint: "/Groups",
144
- schema: SCIM_GROUP_SCHEMA_ID
145
- }];
146
- const handleStaticScimCollection = (items, resourceId, opts) => {
147
- if (resourceId !== void 0) {
148
- const item = items.find((entry) => entry[opts.by] === decodeURIComponent(resourceId));
149
- return item ? scimJson(item) : scimError(404, "notFound", opts.notFound);
150
- }
151
- return scimJson({
152
- schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
153
- Resources: items,
154
- totalResults: items.length,
155
- startIndex: 1,
156
- itemsPerPage: items.length
157
- });
158
- };
159
- const filterScimCollection = (items, filter, filters) => {
160
- if (!filter) return items;
161
- const predicate = filters[filter.attribute];
162
- if (!predicate) throw new Error("Unsupported SCIM filter.");
163
- return items.filter((item) => predicate(item, filter.value));
164
- };
165
- const paginateScimCollection = (items, listRequest) => {
166
- const start = listRequest.startIndex - 1;
167
- return items.slice(start, start + listRequest.count);
168
- };
169
- const requireScimResourceId = (resourceId, label) => {
170
- if (!resourceId) return scimError(400, "invalidPath", `${label} resource ID is required.`);
171
- return null;
172
- };
173
- const readScimJson = async (request) => await request.json();
174
- const auth = {
175
- user: {
176
- current: async (ctx, request) => {
177
- const identity = await ctx.auth.getUserIdentity();
178
- if (identity !== null) {
179
- const [userId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
180
- return userId;
181
- }
182
- if (request !== void 0 && "runMutation" in ctx && ctx.runMutation) {
183
- const authHeader = request.headers.get("Authorization");
184
- if (authHeader?.startsWith("Bearer sk_")) {
185
- const rawKey = authHeader.slice(7);
186
- try {
187
- return (await auth.key.verify(ctx, rawKey)).userId;
188
- } catch {
189
- return null;
190
- }
191
- }
192
- }
193
- return null;
194
- },
195
- require: async (ctx, request) => {
196
- const userId = await auth.user.current(ctx, request);
197
- if (userId === null) throw new AuthError("NOT_SIGNED_IN").toConvexError();
198
- return userId;
199
- },
200
- get: async (ctx, userId) => {
201
- return await ctx.runQuery(config.component.public.userGetById, { userId });
202
- },
203
- list: async (ctx, opts = {}) => {
204
- return await ctx.runQuery(config.component.public.userList, opts);
205
- },
206
- viewer: async (ctx) => {
207
- const userId = await auth.user.current(ctx);
208
- if (userId === null) return null;
209
- return await ctx.runQuery(config.component.public.userGetById, { userId });
210
- },
211
- patch: async (ctx, userId, data) => {
212
- await ctx.runMutation(config.component.public.userPatch, {
213
- userId,
214
- data
215
- });
216
- },
217
- setActiveGroup: async (ctx, opts) => {
218
- const user = await auth.user.get(ctx, opts.userId);
219
- const existingExtend = user !== null && user.extend !== null && typeof user.extend === "object" && !Array.isArray(user.extend) ? { ...user.extend } : {};
220
- if (opts.groupId === null) {
221
- const { lastActiveGroup: _omit, ...rest } = existingExtend;
222
- await auth.user.patch(ctx, opts.userId, { extend: rest });
223
- return;
224
- }
225
- await auth.user.patch(ctx, opts.userId, { extend: {
226
- ...existingExtend,
227
- lastActiveGroup: opts.groupId
228
- } });
229
- },
230
- getActiveGroup: async (ctx, opts) => {
231
- const user = await auth.user.get(ctx, opts.userId);
232
- if (user !== null && user.extend !== null && typeof user.extend === "object" && !Array.isArray(user.extend)) {
233
- const val = user.extend.lastActiveGroup;
234
- if (typeof val === "string") return val;
235
- }
236
- return null;
237
- },
238
- remove: async (ctx, userId, opts) => {
239
- const cascade = opts?.cascade !== false;
240
- const [sessions, accounts, keys, members, passkeys, totps] = await Promise.all([
241
- ctx.runQuery(config.component.public.sessionListByUser, { userId }),
242
- ctx.runQuery(config.component.public.accountListByUser, { userId }),
243
- ctx.runQuery(config.component.public.keyListByUserId, { userId }),
244
- ctx.runQuery(config.component.public.memberListByUser, { userId }),
245
- ctx.runQuery(config.component.public.passkeyListByUserId, { userId }),
246
- ctx.runQuery(config.component.public.totpListByUserId, { userId })
247
- ]);
248
- const totalLinked = sessions.length + accounts.length + keys.length + members.length + passkeys.length + totps.length;
249
- if (!cascade && totalLinked > 0) throw new AuthError("INVALID_PARAMETERS", `Cannot delete user with ${totalLinked} linked records. Pass { cascade: true } to delete all linked records, or remove them manually first.`).toConvexError();
250
- const deletions = [];
251
- for (const s of sessions) deletions.push(ctx.runMutation(config.component.public.sessionDelete, { sessionId: s._id }));
252
- for (const a of accounts) deletions.push(ctx.runMutation(config.component.public.accountDelete, { accountId: a._id }));
253
- for (const k of keys) deletions.push(ctx.runMutation(config.component.public.keyDelete, { keyId: k._id }));
254
- for (const m of members) deletions.push(ctx.runMutation(config.component.public.memberRemove, { memberId: m._id }));
255
- for (const p of passkeys) deletions.push(ctx.runMutation(config.component.public.passkeyDelete, { passkeyId: p._id }));
256
- for (const t of totps) deletions.push(ctx.runMutation(config.component.public.totpDelete, { totpId: t._id }));
257
- await Promise.all(deletions);
258
- await ctx.runMutation(config.component.public.userDelete, { userId });
259
- }
260
- },
261
- session: {
262
- current: async (ctx) => {
263
- const identity = await ctx.auth.getUserIdentity();
264
- if (identity === null) return null;
265
- const [, sessionId] = identity.subject.split(TOKEN_SUB_CLAIM_DIVIDER);
266
- return sessionId;
267
- },
268
- invalidate: async (ctx, args) => {
269
- return await callInvalidateSessions(ctx, args);
270
- },
271
- get: async (ctx, sessionId) => {
272
- return await ctx.runQuery(config.component.public.sessionGetById, { sessionId });
273
- },
274
- list: async (ctx, opts) => {
275
- return await ctx.runQuery(config.component.public.sessionListByUser, { userId: opts.userId });
276
- }
277
- },
278
- account: {
279
- create: async (ctx, args) => {
280
- return await callCreateAccountFromCredentials(ctx, args);
281
- },
282
- get: async (ctx, args) => {
283
- const result = await callRetrieveAccountWithCredentials(ctx, args);
284
- if (typeof result === "string") throw new AuthError("ACCOUNT_NOT_FOUND", result).toConvexError();
285
- return result;
286
- },
287
- update: async (ctx, args) => {
288
- return await callModifyAccount(ctx, args);
289
- },
290
- remove: async (ctx, accountId) => {
291
- const account = await ctx.runQuery(config.component.public.accountGetById, { accountId });
292
- if (account === null) throw new AuthError("ACCOUNT_NOT_FOUND", "Account not found.").toConvexError();
293
- if ((await ctx.runQuery(config.component.public.accountListByUser, { userId: account.userId })).length <= 1) throw new AuthError("INVALID_PARAMETERS", "Cannot unlink the user's only account. This would lock them out.").toConvexError();
294
- await ctx.runMutation(config.component.public.accountDelete, { accountId });
295
- },
296
- listPasskeys: async (ctx, opts) => {
297
- return await ctx.runQuery(config.component.public.passkeyListByUserId, opts);
298
- },
299
- renamePasskey: async (ctx, passkeyId, name) => {
300
- await ctx.runMutation(config.component.public.passkeyUpdateMeta, {
301
- passkeyId,
302
- data: { name }
303
- });
304
- },
305
- removePasskey: async (ctx, passkeyId) => {
306
- await ctx.runMutation(config.component.public.passkeyDelete, { passkeyId });
307
- },
308
- listTotps: async (ctx, opts) => {
309
- return await ctx.runQuery(config.component.public.totpListByUserId, opts);
310
- },
311
- removeTotp: async (ctx, totpId) => {
312
- await ctx.runMutation(config.component.public.totpDelete, { totpId });
313
- }
314
- },
315
- provider: { signIn: async (ctx, provider, args) => {
316
- const result = await signInImpl(enrichCtx(ctx), materializeProvider(provider), args, {
317
- generateTokens: false,
318
- allowExtraProviders: true
319
- });
320
- return result.kind === "signedIn" ? result.signedIn !== null ? {
321
- userId: result.signedIn.userId,
322
- sessionId: result.signedIn.sessionId
323
- } : null : null;
324
- } },
325
- group: {
326
- create: async (ctx, data) => {
327
- return await ctx.runMutation(config.component.public.groupCreate, data);
328
- },
329
- get: async (ctx, groupId) => {
330
- return await ctx.runQuery(config.component.public.groupGet, { groupId });
331
- },
332
- list: async (ctx, opts) => {
333
- return await ctx.runQuery(config.component.public.groupList, {
334
- where: opts?.where,
335
- limit: opts?.limit,
336
- cursor: opts?.cursor,
337
- orderBy: opts?.orderBy,
338
- order: opts?.order
339
- });
340
- },
341
- update: async (ctx, groupId, data) => {
342
- await ctx.runMutation(config.component.public.groupUpdate, {
343
- groupId,
344
- data
345
- });
346
- },
347
- delete: async (ctx, groupId) => {
348
- await ctx.runMutation(config.component.public.groupDelete, { groupId });
349
- },
350
- ancestors: async (ctx, opts) => {
351
- const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));
352
- const visited = /* @__PURE__ */ new Set();
353
- const ancestors = [];
354
- let cycleDetected = false;
355
- let maxDepthReached = false;
356
- let currentGroupId = opts.groupId;
357
- let depth = 0;
358
- let isFirst = true;
359
- while (currentGroupId !== void 0) {
360
- if (depth > maxDepth) {
361
- maxDepthReached = true;
362
- break;
363
- }
364
- if (visited.has(currentGroupId)) {
365
- cycleDetected = true;
366
- break;
367
- }
368
- visited.add(currentGroupId);
369
- const group = await auth.group.get(ctx, currentGroupId);
370
- if (group === null) break;
371
- if (isFirst) {
372
- isFirst = false;
373
- if (opts.includeSelf) ancestors.push(group);
374
- currentGroupId = group.parentGroupId;
375
- depth += 1;
376
- continue;
377
- }
378
- ancestors.push(group);
379
- currentGroupId = group.parentGroupId;
380
- depth += 1;
381
- }
382
- return {
383
- ancestors,
384
- cycleDetected,
385
- maxDepthReached
386
- };
387
- }
388
- },
389
- member: {
390
- add: async (ctx, data) => {
391
- return await ctx.runMutation(config.component.public.memberAdd, data);
392
- },
393
- get: async (ctx, memberId) => {
394
- return await ctx.runQuery(config.component.public.memberGet, { memberId });
395
- },
396
- getByUserAndGroup: async (ctx, opts) => {
397
- return await ctx.runQuery(config.component.public.memberGetByGroupAndUser, opts);
398
- },
399
- list: async (ctx, opts) => {
400
- return await ctx.runQuery(config.component.public.memberList, {
401
- where: opts?.where,
402
- limit: opts?.limit,
403
- cursor: opts?.cursor,
404
- orderBy: opts?.orderBy,
405
- order: opts?.order
406
- });
407
- },
408
- remove: async (ctx, memberId) => {
409
- await ctx.runMutation(config.component.public.memberRemove, { memberId });
410
- },
411
- update: async (ctx, memberId, data) => {
412
- await ctx.runMutation(config.component.public.memberUpdate, {
413
- memberId,
414
- data
415
- });
416
- },
417
- inherit: async (ctx, opts) => {
418
- const roleFilter = opts.roles !== void 0 && opts.roles.length > 0 ? new Set(opts.roles) : null;
419
- const maxDepth = Math.max(0, Math.floor(opts.maxDepth ?? 32));
420
- const visited = /* @__PURE__ */ new Set();
421
- const traversedGroupIds = [];
422
- let currentGroupId = opts.groupId;
423
- let depth = 0;
424
- let cycleDetected = false;
425
- let maxDepthReached = false;
426
- while (currentGroupId !== void 0) {
427
- if (depth > maxDepth) {
428
- maxDepthReached = true;
429
- break;
430
- }
431
- if (visited.has(currentGroupId)) {
432
- cycleDetected = true;
433
- break;
434
- }
435
- visited.add(currentGroupId);
436
- traversedGroupIds.push(currentGroupId);
437
- const membership = await auth.member.getByUserAndGroup(ctx, {
438
- userId: opts.userId,
439
- groupId: currentGroupId
440
- });
441
- if (membership !== null && (roleFilter === null || roleFilter.has(membership.role))) return {
442
- requestedGroupId: opts.groupId,
443
- matchedGroupId: currentGroupId,
444
- membership,
445
- depth,
446
- isDirect: depth === 0,
447
- isInherited: depth > 0,
448
- traversedGroupIds,
449
- cycleDetected: false,
450
- maxDepthReached: false
451
- };
452
- const group = await auth.group.get(ctx, currentGroupId);
453
- if (group === null || group.parentGroupId === void 0) break;
454
- currentGroupId = group.parentGroupId;
455
- depth += 1;
456
- }
457
- return {
458
- requestedGroupId: opts.groupId,
459
- matchedGroupId: null,
460
- membership: null,
461
- depth: null,
462
- isDirect: false,
463
- isInherited: false,
464
- traversedGroupIds,
465
- cycleDetected,
466
- maxDepthReached
467
- };
468
- },
469
- require: async (ctx, opts) => {
470
- const result = await auth.member.inherit(ctx, opts);
471
- if (result.membership === null) throw new AuthError("FORBIDDEN", `User ${opts.userId} has no membership on group ${opts.groupId} or its ancestors.`).toConvexError();
472
- return {
473
- membership: result.membership,
474
- matchedGroupId: result.matchedGroupId,
475
- isDirect: result.isDirect,
476
- isInherited: result.isInherited,
477
- depth: result.depth
478
- };
479
- }
480
- },
481
- invite: {
482
- create: async (ctx, data) => {
483
- const token = generateRandomString(INVITE_TOKEN_LENGTH, INVITE_TOKEN_ALPHABET);
484
- const tokenHash = await sha256(token);
485
- return {
486
- inviteId: await ctx.runMutation(config.component.public.inviteCreate, {
487
- ...data,
488
- tokenHash,
489
- status: "pending"
490
- }),
491
- token
492
- };
493
- },
494
- get: async (ctx, inviteId) => {
495
- return await ctx.runQuery(config.component.public.inviteGet, { inviteId });
496
- },
497
- token: {
498
- get: async (ctx, token) => {
499
- const tokenHash = await sha256(token);
500
- return await ctx.runQuery(config.component.public.inviteGetByTokenHash, { tokenHash });
501
- },
502
- accept: async (ctx, args) => {
503
- const tokenHash = await sha256(args.token);
504
- return await ctx.runMutation(config.component.public.inviteAcceptByToken, {
505
- tokenHash,
506
- acceptedByUserId: args.acceptedByUserId
507
- });
508
- }
509
- },
510
- list: async (ctx, opts) => {
511
- return await ctx.runQuery(config.component.public.inviteList, {
512
- where: opts?.where,
513
- limit: opts?.limit,
514
- cursor: opts?.cursor,
515
- orderBy: opts?.orderBy,
516
- order: opts?.order
517
- });
518
- },
519
- accept: async (ctx, inviteId, acceptedByUserId) => {
520
- await ctx.runMutation(config.component.public.inviteAccept, {
521
- inviteId,
522
- ...acceptedByUserId ? { acceptedByUserId } : {}
523
- });
524
- },
525
- revoke: async (ctx, inviteId) => {
526
- await ctx.runMutation(config.component.public.inviteRevoke, { inviteId });
527
- }
528
- },
529
- key: {
530
- create: async (ctx, opts) => {
531
- const { raw, hashedKey, displayPrefix } = await generateApiKey("sk_");
532
- return {
533
- keyId: await ctx.runMutation(config.component.public.keyInsert, {
534
- userId: opts.userId,
535
- prefix: displayPrefix,
536
- hashedKey,
537
- name: opts.name,
538
- scopes: opts.scopes,
539
- rateLimit: opts.rateLimit,
540
- expiresAt: opts.expiresAt,
541
- metadata: opts.metadata
542
- }),
543
- raw
544
- };
545
- },
546
- verify: async (ctx, rawKey) => {
547
- const hashedKey = await hashApiKey(rawKey);
548
- const key = await ctx.runQuery(config.component.public.keyGetByHashedKey, { hashedKey });
549
- return Fx.run(Fx.gen(function* () {
550
- yield* Fx.guard(!key, Fx.fail(new AuthError("INVALID_API_KEY")));
551
- const k = key;
552
- yield* Fx.guard(k.revoked, Fx.fail(new AuthError("API_KEY_REVOKED")));
553
- yield* Fx.guard(!!(k.expiresAt && k.expiresAt < Date.now()), Fx.fail(new AuthError("API_KEY_EXPIRED")));
554
- const patchData = { lastUsedAt: Date.now() };
555
- if (k.rateLimit) {
556
- const { limited, newState } = checkKeyRateLimit(k.rateLimit, k.rateLimitState ?? void 0);
557
- yield* Fx.guard(limited, Fx.fail(new AuthError("API_KEY_RATE_LIMITED")));
558
- patchData.rateLimitState = newState;
559
- }
560
- yield* Fx.promise(() => ctx.runMutation(config.component.public.keyPatch, {
561
- keyId: k._id,
562
- data: patchData
563
- }));
564
- return {
565
- userId: k.userId,
566
- keyId: k._id,
567
- scopes: buildScopeChecker(k.scopes)
568
- };
569
- }).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
570
- },
571
- list: async (ctx, opts) => {
572
- return await ctx.runQuery(config.component.public.keyList, {
573
- where: opts?.where,
574
- limit: opts?.limit,
575
- cursor: opts?.cursor,
576
- orderBy: opts?.orderBy,
577
- order: opts?.order
578
- });
579
- },
580
- get: async (ctx, keyId) => {
581
- return await ctx.runQuery(config.component.public.keyGetById, { keyId });
582
- },
583
- update: async (ctx, keyId, data) => {
584
- await ctx.runMutation(config.component.public.keyPatch, {
585
- keyId,
586
- data
587
- });
588
- },
589
- revoke: async (ctx, keyId) => {
590
- await ctx.runMutation(config.component.public.keyPatch, {
591
- keyId,
592
- data: { revoked: true }
593
- });
594
- },
595
- remove: async (ctx, keyId) => {
596
- await ctx.runMutation(config.component.public.keyDelete, { keyId });
597
- },
598
- rotate: async (ctx, keyId, opts) => {
599
- const existing = await ctx.runQuery(config.component.public.keyGetById, { keyId });
600
- if (!existing) throw new AuthError("INVALID_PARAMETERS", "API key not found.").toConvexError();
601
- if (existing.revoked === true) throw new AuthError("API_KEY_REVOKED", "Cannot rotate a key that is already revoked.").toConvexError();
602
- await ctx.runMutation(config.component.public.keyPatch, {
603
- keyId,
604
- data: { revoked: true }
605
- });
606
- return await auth.key.create(ctx, {
607
- userId: existing.userId,
608
- name: opts?.name ?? existing.name,
609
- scopes: existing.scopes ?? [],
610
- rateLimit: existing.rateLimit,
611
- expiresAt: opts?.expiresAt,
612
- metadata: existing.metadata
613
- });
614
- }
615
- },
616
- sso: {
617
- connection: {
618
- create: async (ctx, data) => {
619
- return await ctx.runMutation(config.component.public.enterpriseCreate, data);
620
- },
621
- get: async (ctx, enterpriseId) => {
622
- return await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
623
- },
624
- getByGroup: async (ctx, groupId) => {
625
- return await ctx.runQuery(config.component.public.enterpriseGetByGroup, { groupId });
626
- },
627
- getByDomain: async (ctx, domain) => {
628
- return await ctx.runQuery(config.component.public.enterpriseGetByDomain, { domain: normalizeDomain(domain) });
629
- },
630
- list: async (ctx, opts) => {
631
- return await ctx.runQuery(config.component.public.enterpriseList, {
632
- where: opts?.where,
633
- limit: opts?.limit,
634
- cursor: opts?.cursor,
635
- orderBy: opts?.orderBy,
636
- order: opts?.order
637
- });
638
- },
639
- update: async (ctx, enterpriseId, data) => {
640
- await ctx.runMutation(config.component.public.enterpriseUpdate, {
641
- enterpriseId,
642
- data
643
- });
644
- },
645
- remove: async (ctx, enterpriseId) => {
646
- await ctx.runMutation(config.component.public.enterpriseDelete, { enterpriseId });
647
- },
648
- status: async (ctx, enterpriseId) => {
649
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
650
- if (!enterprise) throw new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError).toConvexError();
651
- const protocols = enterprise.config?.protocols ?? {};
652
- const oidcConfig = protocols.oidc;
653
- const samlConfig = protocols.saml;
654
- const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByEnterprise, { enterpriseId });
655
- const domains = await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId });
656
- const oidcReady = oidcConfig?.enabled === true && typeof oidcConfig?.clientId === "string" && oidcConfig.clientId.length > 0 && (typeof oidcConfig?.issuer === "string" || typeof oidcConfig?.discoveryUrl === "string");
657
- const samlReady = samlConfig?.enabled === true && typeof samlConfig?.idp?.entityId === "string";
658
- const scimReady = scimConfig !== null && scimConfig !== void 0 && scimConfig.status === "active";
659
- const ready = enterprise.status === "active" && (oidcReady || samlReady);
660
- return {
661
- enterpriseId: enterprise._id,
662
- status: enterprise.status,
663
- ready,
664
- domainCount: domains.length,
665
- protocols: {
666
- oidc: {
667
- configured: oidcReady,
668
- ready: oidcReady,
669
- clientId: oidcConfig?.clientId ?? null,
670
- issuer: oidcConfig?.issuer ?? oidcConfig?.discoveryUrl ?? null
671
- },
672
- saml: {
673
- configured: samlReady,
674
- ready: samlReady,
675
- entityId: samlConfig?.idp?.entityId ?? null
676
- },
677
- scim: {
678
- configured: scimReady,
679
- ready: scimReady,
680
- basePath: scimConfig?.basePath ?? null,
681
- deprovisionMode: scimConfig?.deprovisionMode ?? null
682
- }
683
- }
684
- };
685
- }
686
- },
687
- domain: {
688
- add: async (ctx, data) => {
689
- return await ctx.runMutation(config.component.public.enterpriseDomainAdd, {
690
- ...data,
691
- domain: normalizeDomain(data.domain)
692
- });
693
- },
694
- list: async (ctx, enterpriseId) => {
695
- return await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId });
696
- },
697
- remove: async (ctx, domainId) => {
698
- await ctx.runMutation(config.component.public.enterpriseDomainDelete, { domainId });
699
- }
700
- },
701
- saml: {
702
- configure: async (ctx, data) => {
703
- return await Fx.run(Fx.gen(function* () {
704
- const enterprise = yield* Fx.from({
705
- ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId }),
706
- err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
707
- }).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent)));
708
- const metadataXml = yield* data.metadataXml ? Fx.succeed(data.metadataXml) : data.metadataUrl ? Fx.defer(() => Fx.from({
709
- ok: async () => {
710
- const response = await fetch(data.metadataUrl);
711
- if (!response.ok) throw new Error(`Failed to fetch SAML metadata: ${response.status}`);
712
- return await response.text();
713
- },
714
- err: (error) => new AuthError("INVALID_PARAMETERS", error instanceof Error ? error.message : "Failed to fetch SAML metadata")
715
- })).pipe(Fx.timeout(1e4), Fx.retry(Fx.retry.compose(Fx.retry.jittered(Fx.retry.exponential(200)), Fx.retry.recurs(2))), Fx.recover((error) => Fx.fail(new AuthError("INVALID_PARAMETERS", error instanceof Error ? error.message : "Failed to fetch SAML metadata")))) : Fx.fail(new AuthError("INVALID_PARAMETERS", "SAML registration requires metadataXml or metadataUrl."));
716
- const parsed = yield* Fx.from({
717
- ok: () => parseSamlIdpMetadata(metadataXml),
718
- err: () => new AuthError("INVALID_PARAMETERS", "Failed to parse SAML metadata.")
719
- });
720
- const baseConfig = upsertProtocolConfig(enterprise.config, "saml", {
721
- enabled: true,
722
- idp: {
723
- metadataXml,
724
- ...parsed
725
- },
726
- sp: data.sp,
727
- signAuthnRequests: data.signAuthnRequests ?? parsed.wantsSignedAuthnRequests,
728
- attributeMapping: data.attributeMapping,
729
- accountLinking: "verifiedEmail",
730
- reuseScimUserBy: "externalId"
731
- });
732
- const normalizedDomains = data.domains?.map(normalizeDomain);
733
- const nextConfig = normalizedDomains ? {
734
- ...baseConfig,
735
- domains: normalizedDomains
736
- } : baseConfig;
737
- yield* Fx.from({
738
- ok: () => ctx.runMutation(config.component.public.enterpriseUpdate, {
739
- enterpriseId: enterprise._id,
740
- data: {
741
- status: "active",
742
- config: nextConfig
743
- }
744
- }),
745
- err: () => new AuthError("INTERNAL_ERROR", "Failed to persist SAML registration.")
746
- });
747
- if (normalizedDomains) for (const [index, domain] of normalizedDomains.entries()) yield* Fx.from({
748
- ok: () => ctx.runMutation(config.component.public.enterpriseDomainAdd, {
749
- enterpriseId: enterprise._id,
750
- groupId: enterprise.groupId,
751
- domain,
752
- isPrimary: index === 0
753
- }),
754
- err: () => new AuthError("INTERNAL_ERROR", "Failed to persist enterprise domain.")
755
- });
756
- yield* Fx.from({
757
- ok: () => recordEnterpriseAuditEvent(ctx, {
758
- enterpriseId: enterprise._id,
759
- groupId: enterprise.groupId,
760
- eventType: "enterprise.saml.registered",
761
- actorType: "system",
762
- subjectType: "enterprise_saml",
763
- subjectId: enterprise._id,
764
- ok: true,
765
- metadata: {
766
- metadataUrl: data.metadataUrl,
767
- domains: normalizedDomains
768
- }
769
- }),
770
- err: () => new AuthError("INTERNAL_ERROR", "Failed to record SAML registration audit event.")
771
- });
772
- return {
773
- enterpriseId: enterprise._id,
774
- groupId: enterprise.groupId
775
- };
776
- }).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
777
- },
778
- metadata: async (ctx, opts) => {
779
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: opts.enterpriseId });
780
- if (!enterprise) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
781
- return createServiceProviderMetadata(getSamlServiceProviderOptions({
782
- rootUrl: requireEnv("CONVEX_SITE_URL"),
783
- source: {
784
- kind: "enterprise",
785
- id: enterprise._id
786
- },
787
- config: enterprise.config,
788
- overrides: {
789
- entityId: opts.entityId,
790
- acsUrl: opts.acsUrl,
791
- sloUrl: opts.sloUrl
792
- }
793
- }));
794
- },
795
- validate: async (ctx, enterpriseId) => {
796
- const checks = [];
797
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
798
- if (!enterprise) return {
799
- ok: false,
800
- enterpriseId,
801
- checks: [{
802
- name: "enterprise_exists",
803
- ok: false,
804
- message: "Enterprise not found."
805
- }]
806
- };
807
- const samlConfig = enterprise.config?.protocols?.saml;
808
- const samlConfigured = samlConfig?.enabled === true && typeof samlConfig?.idp?.metadataXml === "string";
809
- checks.push({
810
- name: "saml_configured",
811
- ok: samlConfigured,
812
- message: samlConfigured ? void 0 : "SAML is not configured."
813
- });
814
- const hasIdpMetadata = typeof samlConfig?.idp?.metadataXml === "string" && samlConfig.idp.metadataXml.length > 0;
815
- checks.push({
816
- name: "idp_metadata_present",
817
- ok: hasIdpMetadata,
818
- message: hasIdpMetadata ? void 0 : "IdP metadata XML is missing."
819
- });
820
- const hasEntityId = typeof samlConfig?.idp?.entityId === "string" && samlConfig.idp.entityId.length > 0;
821
- checks.push({
822
- name: "idp_entity_id",
823
- ok: hasEntityId,
824
- message: hasEntityId ? void 0 : "IdP entityId could not be parsed from metadata."
825
- });
826
- let spMetadataOk = false;
827
- let spMetadataMessage;
828
- if (samlConfigured) try {
829
- createServiceProviderMetadata(getSamlServiceProviderOptions({
830
- rootUrl: requireEnv("CONVEX_SITE_URL"),
831
- source: {
832
- kind: "enterprise",
833
- id: enterprise._id
834
- },
835
- config: enterprise.config,
836
- overrides: {}
837
- }));
838
- spMetadataOk = true;
839
- } catch (e) {
840
- spMetadataMessage = e instanceof Error ? e.message : "SP metadata generation failed.";
841
- }
842
- else spMetadataMessage = "Skipped — SAML not configured.";
843
- checks.push({
844
- name: "sp_metadata_generates",
845
- ok: spMetadataOk,
846
- message: spMetadataMessage
847
- });
848
- return {
849
- ok: checks.every((c) => c.ok),
850
- enterpriseId: enterprise._id,
851
- checks
852
- };
853
- }
854
- },
855
- oidc: {
856
- configure: async (ctx, data) => {
857
- return await Fx.run(Fx.gen(function* () {
858
- yield* Fx.guard(data.issuer === void 0 && data.discoveryUrl === void 0, Fx.fail(new AuthError("INVALID_PARAMETERS", "OIDC registration requires issuer or discoveryUrl.")));
859
- const enterprise = yield* Fx.from({
860
- ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId }),
861
- err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
862
- }).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent)));
863
- const nextConfig = upsertProtocolConfig(enterprise.config, "oidc", {
864
- enabled: true,
865
- issuer: data.issuer,
866
- discoveryUrl: data.discoveryUrl,
867
- clientId: data.clientId,
868
- clientSecret: data.clientSecret,
869
- scopes: data.scopes ?? [
870
- "openid",
871
- "profile",
872
- "email"
873
- ],
874
- authorizationParams: data.authorizationParams,
875
- accountLinking: "verifiedEmail",
876
- reuseScimUserBy: "externalId",
877
- clockToleranceSeconds: data.clockToleranceSeconds,
878
- strictIssuer: data.strictIssuer,
879
- extraFields: data.extraFields
880
- });
881
- yield* Fx.from({
882
- ok: () => ctx.runMutation(config.component.public.enterpriseUpdate, {
883
- enterpriseId: data.enterpriseId,
884
- data: { config: nextConfig }
885
- }),
886
- err: () => new AuthError("INTERNAL_ERROR", "Failed to persist OIDC registration.")
887
- });
888
- yield* Fx.from({
889
- ok: () => recordEnterpriseAuditEvent(ctx, {
890
- enterpriseId: data.enterpriseId,
891
- groupId: enterprise.groupId,
892
- eventType: "enterprise.oidc.registered",
893
- actorType: "system",
894
- subjectType: "enterprise_oidc",
895
- subjectId: data.enterpriseId,
896
- ok: true,
897
- metadata: {
898
- issuer: data.issuer,
899
- discoveryUrl: data.discoveryUrl
900
- }
901
- }),
902
- err: () => new AuthError("INTERNAL_ERROR", "Failed to record OIDC registration audit event.")
903
- });
904
- return getOidcConfig(nextConfig);
905
- }).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
906
- },
907
- get: async (ctx, enterpriseId) => {
908
- return await Fx.run(Fx.from({
909
- ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId }),
910
- err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
911
- }).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent)), Fx.map((enterprise) => getOidcConfig(enterprise.config)), Fx.recover((e) => Fx.fatal(e.toConvexError()))));
912
- },
913
- resolveSignIn: async (ctx, data) => {
914
- return await Fx.run(Fx.gen(function* () {
915
- const enterprise = data.enterpriseId !== void 0 ? yield* Fx.from({
916
- ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId }),
917
- err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
918
- }).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent))) : data.domain !== void 0 || data.email !== void 0 ? yield* Fx.from({
919
- ok: () => ctx.runQuery(config.component.public.enterpriseGetByDomain, { domain: normalizeDomain(data.domain ?? String(data.email).split("@").at(-1) ?? "") }),
920
- err: () => new AuthError("INTERNAL_ERROR", "Failed to resolve enterprise by domain.")
921
- }).pipe(Fx.chain((result) => result?.enterprise ? Fx.succeed(result.enterprise) : Fx.fail(new AuthError("INVALID_PARAMETERS", "No enterprise OIDC connection matched the provided input.")))) : yield* Fx.fail(new AuthError("INVALID_PARAMETERS", "No enterprise OIDC connection matched the provided input."));
922
- yield* Fx.guard(enterprise.status !== "active", Fx.fail(new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.")));
923
- const oidc = getOidcConfig(enterprise.config);
924
- yield* Fx.guard(oidc.enabled !== true, Fx.fail(new AuthError("PROVIDER_NOT_CONFIGURED", "OIDC is not configured for this enterprise.")));
925
- const urls = getEnterpriseOidcUrls({
926
- rootUrl: requireEnv("CONVEX_SITE_URL"),
927
- enterpriseId: enterprise._id
928
- });
929
- return {
930
- enterpriseId: enterprise._id,
931
- providerId: enterpriseOidcProviderId(enterprise._id),
932
- signInPath: urls.signInUrl,
933
- callbackPath: urls.callbackUrl,
934
- redirectTo: data.redirectTo
935
- };
936
- }).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
937
- },
938
- validate: async (ctx, enterpriseId) => {
939
- const checks = [];
940
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
941
- if (!enterprise) return {
942
- ok: false,
943
- enterpriseId,
944
- checks: [{
945
- name: "enterprise_exists",
946
- ok: false,
947
- message: "Enterprise not found."
948
- }]
949
- };
950
- const oidc = getOidcConfig(enterprise.config);
951
- const oidcConfigured = oidc.enabled === true && typeof oidc.clientId === "string" && oidc.clientId.length > 0;
952
- checks.push({
953
- name: "oidc_configured",
954
- ok: oidcConfigured,
955
- message: oidcConfigured ? void 0 : "OIDC is not configured."
956
- });
957
- const hasClientId = typeof oidc.clientId === "string" && oidc.clientId.length > 0;
958
- checks.push({
959
- name: "client_id_present",
960
- ok: hasClientId,
961
- message: hasClientId ? void 0 : "clientId is missing."
962
- });
963
- const discoveryTarget = oidc.discoveryUrl ?? oidc.issuer;
964
- const hasDiscovery = typeof discoveryTarget === "string" && discoveryTarget.length > 0;
965
- checks.push({
966
- name: "issuer_or_discovery_url_present",
967
- ok: hasDiscovery,
968
- message: hasDiscovery ? void 0 : "issuer or discoveryUrl is missing."
969
- });
970
- let discoveryOk = false;
971
- let discoveryMessage;
972
- if (hasDiscovery) {
973
- const discoveryUrl = oidc.discoveryUrl?.length ? oidc.discoveryUrl : `${oidc.issuer}/.well-known/openid-configuration`;
974
- try {
975
- const res = await fetch(discoveryUrl, {
976
- headers: { Accept: "application/json" },
977
- signal: AbortSignal.timeout(8e3)
978
- });
979
- if (!res.ok) discoveryMessage = `Discovery endpoint returned ${res.status}.`;
980
- else {
981
- const json = await res.json();
982
- if (typeof json.issuer !== "string") discoveryMessage = "Discovery document is missing issuer field.";
983
- else if (typeof json.authorization_endpoint !== "string") discoveryMessage = "Discovery document is missing authorization_endpoint.";
984
- else discoveryOk = true;
985
- }
986
- } catch (e) {
987
- discoveryMessage = e instanceof Error ? `Discovery fetch failed: ${e.message}` : "Discovery fetch failed.";
988
- }
989
- } else discoveryMessage = "Skipped — issuer or discoveryUrl not set.";
990
- checks.push({
991
- name: "discovery_reachable",
992
- ok: discoveryOk,
993
- message: discoveryMessage
994
- });
995
- return {
996
- ok: checks.every((c) => c.ok),
997
- enterpriseId: enterprise._id,
998
- checks
999
- };
1000
- }
1001
- },
1002
- scim: {
1003
- configure: async (ctx, data) => {
1004
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId });
1005
- if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1006
- const rawToken = generateRandomString(48, INVITE_TOKEN_ALPHABET);
1007
- const tokenHash = await sha256(rawToken);
1008
- const configId = await ctx.runMutation(config.component.public.enterpriseScimConfigUpsert, {
1009
- enterpriseId: enterprise._id,
1010
- groupId: enterprise.groupId,
1011
- status: data.status ?? "active",
1012
- basePath: data.basePath ?? `${requireEnv("CONVEX_SITE_URL")}/api/auth/sso/${enterprise._id}/scim/v2`,
1013
- tokenHash,
1014
- lastRotatedAt: Date.now(),
1015
- deprovisionMode: data.deprovisionMode ?? "soft"
1016
- });
1017
- const auditEventId = await recordEnterpriseAuditEvent(ctx, {
1018
- enterpriseId: enterprise._id,
1019
- groupId: enterprise.groupId,
1020
- eventType: "enterprise.scim.configured",
1021
- actorType: "system",
1022
- subjectType: "enterprise_scim",
1023
- subjectId: configId,
1024
- ok: true
1025
- });
1026
- await emitEnterpriseWebhookDeliveries(ctx, {
1027
- enterpriseId: enterprise._id,
1028
- eventType: "enterprise.scim.configured",
1029
- auditEventId,
1030
- payload: {
1031
- enterpriseId: enterprise._id,
1032
- scimConfigId: configId
1033
- }
1034
- });
1035
- return {
1036
- token: rawToken,
1037
- configId
1038
- };
1039
- },
1040
- get: async (ctx, enterpriseId) => {
1041
- return await ctx.runQuery(config.component.public.enterpriseScimConfigGetByEnterprise, { enterpriseId });
1042
- },
1043
- getConfigByToken: async (ctx, token) => {
1044
- return await ctx.runQuery(config.component.public.enterpriseScimConfigGetByTokenHash, { tokenHash: await sha256(token) });
1045
- },
1046
- validate: async (ctx, enterpriseId) => {
1047
- const checks = [];
1048
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
1049
- if (!enterprise) return {
1050
- ok: false,
1051
- enterpriseId,
1052
- checks: [{
1053
- name: "enterprise_exists",
1054
- ok: false,
1055
- message: "Enterprise not found."
1056
- }]
1057
- };
1058
- const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByEnterprise, { enterpriseId });
1059
- const hasConfig = scimConfig !== null && scimConfig !== void 0;
1060
- checks.push({
1061
- name: "scim_config_exists",
1062
- ok: hasConfig,
1063
- message: hasConfig ? void 0 : "SCIM has not been configured."
1064
- });
1065
- const isActive = hasConfig && scimConfig.status === "active";
1066
- checks.push({
1067
- name: "scim_config_active",
1068
- ok: isActive,
1069
- message: isActive ? void 0 : `SCIM config status is ${hasConfig ? scimConfig.status : "unknown"}.`
1070
- });
1071
- const hasToken = hasConfig && typeof scimConfig.tokenHash === "string" && scimConfig.tokenHash.length > 0;
1072
- checks.push({
1073
- name: "token_hash_set",
1074
- ok: hasToken,
1075
- message: hasToken ? void 0 : "SCIM bearer token has not been set."
1076
- });
1077
- const hasBasePath = hasConfig && typeof scimConfig.basePath === "string" && scimConfig.basePath.length > 0;
1078
- checks.push({
1079
- name: "base_path_set",
1080
- ok: hasBasePath,
1081
- message: hasBasePath ? void 0 : "SCIM basePath is missing."
1082
- });
1083
- return {
1084
- ok: checks.every((c) => c.ok),
1085
- enterpriseId: enterprise._id,
1086
- basePath: hasBasePath ? scimConfig.basePath : null,
1087
- deprovisionMode: hasConfig ? scimConfig.deprovisionMode : null,
1088
- checks
1089
- };
1090
- },
1091
- identity: {
1092
- get: async (ctx, data) => {
1093
- return await ctx.runQuery(config.component.public.enterpriseScimIdentityGet, data);
1094
- },
1095
- upsert: async (ctx, data) => {
1096
- return await ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
1097
- ...data,
1098
- lastProvisionedAt: Date.now()
1099
- });
1100
- }
1101
- }
1102
- },
1103
- audit: {
1104
- record: async (ctx, data) => {
1105
- return await recordEnterpriseAuditEvent(ctx, data);
1106
- },
1107
- list: async (ctx, data) => {
1108
- return await ctx.runQuery(config.component.public.enterpriseAuditEventList, data);
1109
- }
1110
- },
1111
- webhook: {
1112
- endpoint: {
1113
- create: async (ctx, data) => {
1114
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId });
1115
- if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1116
- const secretHash = await sha256(data.secret);
1117
- const endpointId = await ctx.runMutation(config.component.public.enterpriseWebhookEndpointCreate, {
1118
- enterpriseId: enterprise._id,
1119
- groupId: enterprise.groupId,
1120
- url: data.url,
1121
- secretHash,
1122
- subscriptions: data.subscriptions,
1123
- createdByUserId: data.createdByUserId
1124
- });
1125
- await recordEnterpriseAuditEvent(ctx, {
1126
- enterpriseId: enterprise._id,
1127
- groupId: enterprise.groupId,
1128
- eventType: "enterprise.webhook.endpoint.created",
1129
- actorType: data.createdByUserId ? "user" : "system",
1130
- actorId: data.createdByUserId,
1131
- subjectType: "enterprise_webhook_endpoint",
1132
- subjectId: endpointId,
1133
- ok: true
1134
- });
1135
- return { endpointId };
1136
- },
1137
- list: async (ctx, enterpriseId) => {
1138
- return await ctx.runQuery(config.component.public.enterpriseWebhookEndpointList, { enterpriseId });
1139
- },
1140
- disable: async (ctx, endpointId) => {
1141
- await ctx.runMutation(config.component.public.enterpriseWebhookEndpointUpdate, {
1142
- endpointId,
1143
- data: { status: "disabled" }
1144
- });
1145
- }
1146
- },
1147
- emit: async (ctx, data) => {
1148
- await emitEnterpriseWebhookDeliveries(ctx, data);
1149
- },
1150
- delivery: {
1151
- list: async (ctx, data) => {
1152
- return await ctx.runQuery(config.component.public.enterpriseWebhookDeliveryList, data);
1153
- },
1154
- listReady: async (ctx, limit) => {
1155
- return await ctx.runQuery(config.component.public.enterpriseWebhookDeliveryListReady, {
1156
- now: Date.now(),
1157
- limit
1158
- });
1159
- },
1160
- markDelivered: async (ctx, deliveryId, responseStatus) => {
1161
- await ctx.runMutation(config.component.public.enterpriseWebhookDeliveryPatch, {
1162
- deliveryId,
1163
- data: {
1164
- status: "delivered",
1165
- attemptCount: 1,
1166
- lastAttemptAt: Date.now(),
1167
- lastResponseStatus: responseStatus
1168
- }
1169
- });
1170
- },
1171
- markFailed: async (ctx, deliveryId, data) => {
1172
- await ctx.runMutation(config.component.public.enterpriseWebhookDeliveryPatch, {
1173
- deliveryId,
1174
- data: {
1175
- status: data.retryAt ? "pending" : "failed",
1176
- attemptCount: data.attemptCount,
1177
- lastAttemptAt: Date.now(),
1178
- lastResponseStatus: data.responseStatus,
1179
- lastError: data.error,
1180
- nextAttemptAt: data.retryAt ?? Date.now()
1181
- }
1182
- });
1183
- }
1184
- }
1185
- }
1186
- },
1187
- http: {
1188
- add: (http) => {
1189
- http.route({
1190
- path: "/.well-known/openid-configuration",
1191
- method: "GET",
1192
- handler: httpActionGeneric(async () => {
1193
- return new Response(JSON.stringify({
1194
- issuer: requireEnv("CONVEX_SITE_URL"),
1195
- jwks_uri: requireEnv("CONVEX_SITE_URL") + "/.well-known/jwks.json",
1196
- authorization_endpoint: requireEnv("CONVEX_SITE_URL") + "/oauth/authorize"
1197
- }), {
1198
- status: 200,
1199
- headers: {
1200
- "Content-Type": "application/json",
1201
- "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"
1202
- }
1203
- });
1204
- })
1205
- });
1206
- http.route({
1207
- path: "/.well-known/jwks.json",
1208
- method: "GET",
1209
- handler: httpActionGeneric(async () => {
1210
- return new Response(requireEnv("JWKS"), {
1211
- status: 200,
1212
- headers: {
1213
- "Content-Type": "application/json",
1214
- "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"
1215
- }
1216
- });
1217
- })
1218
- });
1219
- if (hasSSO) {
1220
- http.route({
1221
- pathPrefix: `${ENTERPRISE_CONTROL_ROUTE_BASE}/`,
1222
- method: "GET",
1223
- handler: httpActionGeneric(convertErrorsToResponse(400, async (ctx, request) => {
1224
- const runtimePathname = new URL(request.url).pathname;
1225
- const runtimePrefix = `${ENTERPRISE_CONTROL_ROUTE_BASE}/`;
1226
- const [runtimeEnterpriseId, protocol, ...rest] = runtimePathname.startsWith(runtimePrefix) ? runtimePathname.slice(runtimePrefix.length).split("/").filter(Boolean) : [];
1227
- const runtimeRoute = runtimeEnterpriseId !== void 0 && (protocol === "oidc" || protocol === "saml" || protocol === "scim") && rest.length > 0 ? {
1228
- enterpriseId: runtimeEnterpriseId,
1229
- protocol,
1230
- rest
1231
- } : null;
1232
- if (!runtimeRoute) throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1233
- if (runtimeRoute.protocol === "saml" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "metadata") {
1234
- const enterpriseDoc = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: runtimeRoute.enterpriseId });
1235
- if (enterpriseDoc === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1236
- const loaded = {
1237
- source: {
1238
- kind: "enterprise",
1239
- id: runtimeRoute.enterpriseId
1240
- },
1241
- config: enterpriseDoc.config,
1242
- status: enterpriseDoc.status
1243
- };
1244
- if (!isEnterpriseSamlSourceActive(loaded)) throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
1245
- if (!getSamlConfig(loaded.config).idp?.metadataXml) throw new AuthError("PROVIDER_NOT_CONFIGURED", "SAML is not configured for this enterprise.").toConvexError();
1246
- return new Response(createEnterpriseSamlMetadataXml({
1247
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1248
- source: loaded.source,
1249
- config: loaded.config
1250
- }), {
1251
- status: 200,
1252
- headers: { "Content-Type": "application/xml" }
1253
- });
1254
- }
1255
- if (runtimeRoute.protocol === "saml" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "signin") {
1256
- const url = new URL(request.url);
1257
- const verifier = url.searchParams.get("code");
1258
- if (!verifier) throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
1259
- const enterpriseDoc = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: runtimeRoute.enterpriseId });
1260
- if (enterpriseDoc === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1261
- const loaded = {
1262
- source: {
1263
- kind: "enterprise",
1264
- id: runtimeRoute.enterpriseId
1265
- },
1266
- config: enterpriseDoc.config,
1267
- status: enterpriseDoc.status,
1268
- enterprise: enterpriseDoc
1269
- };
1270
- if (!isEnterpriseSamlSourceActive(loaded)) throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
1271
- if (!getSamlConfig(loaded.config).idp?.metadataXml) throw new AuthError("PROVIDER_NOT_CONFIGURED", "SAML is not configured for this enterprise.").toConvexError();
1272
- const enterprise = loaded.enterprise;
1273
- const state = generateRandomString(24, INVITE_TOKEN_ALPHABET);
1274
- const signInRequest = createEnterpriseSamlSignInRequest({
1275
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1276
- source: {
1277
- kind: "enterprise",
1278
- id: enterprise._id
1279
- },
1280
- config: loaded.config,
1281
- state,
1282
- signature: `saml ${enterprise._id} pending ${state}`,
1283
- redirectTo: url.searchParams.get("redirectTo") ?? void 0
1284
- });
1285
- const signature = `saml ${enterprise._id} ${signInRequest.requestId} ${state}`;
1286
- await callVerifierSignature(ctx, {
1287
- verifier,
1288
- signature
1289
- });
1290
- const redirectTo = url.searchParams.get("redirectTo");
1291
- const redirectCookies = redirectTo !== null ? [redirectToParamCookie(enterpriseSamlProviderId(enterprise._id), redirectTo)] : [];
1292
- const relayState = encodeEnterpriseSamlRelayState({
1293
- source: {
1294
- kind: "enterprise",
1295
- id: enterprise._id
1296
- },
1297
- signature,
1298
- requestId: signInRequest.requestId,
1299
- state,
1300
- redirectTo: url.searchParams.get("redirectTo") ?? void 0
1301
- });
1302
- if (signInRequest.binding === "redirect" && signInRequest.redirectUrl) {
1303
- const redirectUrl = new URL(signInRequest.redirectUrl);
1304
- redirectUrl.searchParams.set("RelayState", relayState);
1305
- const headers = new Headers({ Location: redirectUrl.toString() });
1306
- for (const { name, value, options } of redirectCookies) headers.append("Set-Cookie", serialize(name, value, options));
1307
- return new Response(null, {
1308
- status: 302,
1309
- headers
1310
- });
1311
- }
1312
- const response = createSamlPostBindingResponse({
1313
- endpoint: signInRequest.post.endpoint,
1314
- parameter: "SAMLRequest",
1315
- value: signInRequest.post.value,
1316
- relayState
1317
- });
1318
- for (const { name, value, options } of redirectCookies) response.headers.append("Set-Cookie", serialize(name, value, options));
1319
- return response;
1320
- }
1321
- if (runtimeRoute.protocol === "saml" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "acs") return await samlAcsHandler(ctx, request);
1322
- if (runtimeRoute.protocol === "saml" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "slo") return await samlSloHandler(ctx, request);
1323
- if (runtimeRoute.protocol === "oidc" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "signin") {
1324
- const url = new URL(request.url);
1325
- const verifier = url.searchParams.get("code");
1326
- if (!verifier) throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
1327
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: runtimeRoute.enterpriseId });
1328
- if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1329
- if (enterprise.status !== "active") throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
1330
- if (getOidcConfig(enterprise.config).enabled !== true) throw new AuthError("PROVIDER_NOT_CONFIGURED", "OIDC is not configured for this enterprise.").toConvexError();
1331
- const { providerId, provider, oauthConfig } = await createEnterpriseOidcRuntime({
1332
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1333
- enterpriseId: enterprise._id,
1334
- config: enterprise.config
1335
- });
1336
- const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, provider, oauthConfig);
1337
- await callVerifierSignature(ctx, {
1338
- verifier,
1339
- signature
1340
- });
1341
- const redirectTo = url.searchParams.get("redirectTo");
1342
- const headers_ = new Headers({ Location: redirect });
1343
- for (const { name, value, options } of [...cookies, ...redirectTo !== null ? [redirectToParamCookie(providerId, redirectTo)] : []]) headers_.append("Set-Cookie", serialize(name, value, options));
1344
- return new Response(null, {
1345
- status: 302,
1346
- headers: headers_
1347
- });
1348
- }
1349
- if (runtimeRoute.protocol === "oidc" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "callback") {
1350
- const url = new URL(request.url);
1351
- const enterpriseId = runtimeRoute.enterpriseId;
1352
- const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
1353
- if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1354
- const oidc = getOidcConfig(enterprise.config);
1355
- const { providerId, provider, oauthConfig } = await createEnterpriseOidcRuntime({
1356
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1357
- enterpriseId: enterprise._id,
1358
- config: enterprise.config
1359
- });
1360
- const cookies = getCookies(request);
1361
- const maybeRedirectTo = useRedirectToParam(providerId, cookies);
1362
- const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
1363
- const params = url.searchParams;
1364
- const result = await Fx.run(handleOAuthCallback(providerId, provider, oauthConfig, Object.fromEntries(params.entries()), cookies));
1365
- const extraFields = oidc.extraFields;
1366
- let profile = result.profile;
1367
- if (extraFields && typeof profile === "object" && profile) {
1368
- const extend = {};
1369
- for (const [claimName, fieldName] of Object.entries(extraFields)) if (claimName in profile) extend[fieldName] = profile[claimName];
1370
- if (Object.keys(extend).length > 0) profile = {
1371
- ...profile,
1372
- extend
1373
- };
1374
- }
1375
- const verificationCode = await callUserOAuth(ctx, {
1376
- provider: providerId,
1377
- providerAccountId: result.providerAccountId,
1378
- profile,
1379
- signature: result.signature,
1380
- accountExtend: { identity: {
1381
- protocol: "oidc",
1382
- enterpriseId: enterprise._id,
1383
- subject: result.providerAccountId,
1384
- issuer: typeof oidc.issuer === "string" ? oidc.issuer : void 0,
1385
- discoveryUrl: typeof oidc.discoveryUrl === "string" ? oidc.discoveryUrl : void 0
1386
- } }
1387
- });
1388
- const headers = new Headers({ Location: setURLSearchParam(destinationUrl, "code", verificationCode) });
1389
- for (const { name, value, options } of result.cookies) headers.append("Set-Cookie", serialize(name, value, options));
1390
- if (maybeRedirectTo) headers.append("Set-Cookie", serialize(maybeRedirectTo.updatedCookie.name, maybeRedirectTo.updatedCookie.value, maybeRedirectTo.updatedCookie.options));
1391
- return new Response(null, {
1392
- status: 302,
1393
- headers
1394
- });
1395
- }
1396
- if (runtimeRoute.protocol === "scim" && runtimeRoute.rest[0] === "v2") return await enterpriseScimHandler(ctx, request);
1397
- throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1398
- }))
1399
- });
1400
- http.route({
1401
- pathPrefix: `${ENTERPRISE_CONTROL_ROUTE_BASE}/`,
1402
- method: "POST",
1403
- handler: httpActionGeneric(convertErrorsToResponse(400, async (ctx, request) => {
1404
- const runtimePathname = new URL(request.url).pathname;
1405
- const runtimePrefix = `${ENTERPRISE_CONTROL_ROUTE_BASE}/`;
1406
- const [runtimeEnterpriseId, protocol, ...rest] = runtimePathname.startsWith(runtimePrefix) ? runtimePathname.slice(runtimePrefix.length).split("/").filter(Boolean) : [];
1407
- const runtimeRoute = runtimeEnterpriseId !== void 0 && (protocol === "oidc" || protocol === "saml" || protocol === "scim") && rest.length > 0 ? {
1408
- pathname: runtimePathname,
1409
- enterpriseId: runtimeEnterpriseId,
1410
- protocol,
1411
- rest
1412
- } : null;
1413
- if (runtimeRoute) {
1414
- if (runtimeRoute.protocol === "saml" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "acs") return await samlAcsHandler(ctx, request);
1415
- if (runtimeRoute.protocol === "saml" && runtimeRoute.rest.length === 1 && runtimeRoute.rest[0] === "slo") return await samlSloHandler(ctx, request);
1416
- if (runtimeRoute.protocol === "scim" && runtimeRoute.rest[0] === "v2") return await enterpriseScimHandler(ctx, request);
1417
- throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1418
- }
1419
- throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1420
- }))
1421
- });
1422
- http.route({
1423
- pathPrefix: `${ENTERPRISE_CONTROL_ROUTE_BASE}/`,
1424
- method: "PUT",
1425
- handler: httpActionGeneric(convertErrorsToResponse(400, async (ctx, request) => {
1426
- const runtimePathname = new URL(request.url).pathname;
1427
- const runtimePrefix = `${ENTERPRISE_CONTROL_ROUTE_BASE}/`;
1428
- const [runtimeEnterpriseId, protocol, ...rest] = runtimePathname.startsWith(runtimePrefix) ? runtimePathname.slice(runtimePrefix.length).split("/").filter(Boolean) : [];
1429
- const runtimeRoute = runtimeEnterpriseId !== void 0 && (protocol === "oidc" || protocol === "saml" || protocol === "scim") && rest.length > 0 ? {
1430
- pathname: runtimePathname,
1431
- enterpriseId: runtimeEnterpriseId,
1432
- protocol,
1433
- rest
1434
- } : null;
1435
- if (runtimeRoute) {
1436
- if (runtimeRoute.protocol === "scim" && runtimeRoute.rest[0] === "v2") return await enterpriseScimHandler(ctx, request);
1437
- throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1438
- }
1439
- throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1440
- }))
1441
- });
1442
- const samlAcsHandler = convertErrorsToResponse(400, async (ctx, request) => Fx.run(Fx.gen(function* () {
1443
- const runtimePathname = new URL(request.url).pathname;
1444
- const runtimePrefix = `${ENTERPRISE_CONTROL_ROUTE_BASE}/`;
1445
- const [runtimeEnterpriseId, protocol, ...rest] = runtimePathname.startsWith(runtimePrefix) ? runtimePathname.slice(runtimePrefix.length).split("/").filter(Boolean) : [];
1446
- const runtimeRoute = runtimeEnterpriseId !== void 0 && (protocol === "oidc" || protocol === "saml" || protocol === "scim") && rest.length > 0 ? {
1447
- pathname: runtimePathname,
1448
- enterpriseId: runtimeEnterpriseId,
1449
- protocol,
1450
- rest
1451
- } : null;
1452
- yield* Fx.guard(!runtimeRoute || runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "acs", Fx.fail(new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError()));
1453
- const enterpriseId = runtimeRoute.enterpriseId;
1454
- const { loaded, saml } = yield* Fx.from({
1455
- ok: async () => {
1456
- const enterprise$1 = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
1457
- if (enterprise$1 === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1458
- const loaded$1 = {
1459
- source: {
1460
- kind: "enterprise",
1461
- id: enterpriseId
1462
- },
1463
- config: enterprise$1.config,
1464
- status: enterprise$1.status,
1465
- enterprise: enterprise$1
1466
- };
1467
- if (!isEnterpriseSamlSourceActive(loaded$1)) throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
1468
- const saml$1 = getSamlConfig(loaded$1.config);
1469
- if (!saml$1.idp?.metadataXml) throw new AuthError("PROVIDER_NOT_CONFIGURED", "SAML is not configured for this enterprise.").toConvexError();
1470
- return {
1471
- loaded: loaded$1,
1472
- saml: saml$1
1473
- };
1474
- },
1475
- err: (e) => e
1476
- });
1477
- const enterprise = loaded.enterprise;
1478
- const parsedResponse = yield* Fx.from({
1479
- ok: () => parseEnterpriseSamlLoginResponse({
1480
- request,
1481
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1482
- source: {
1483
- kind: "enterprise",
1484
- id: enterprise._id
1485
- },
1486
- config: loaded.config
1487
- }),
1488
- err: (e) => new AuthError("OAUTH_PROVIDER_ERROR", `SAML response parse failed: ${e instanceof Error ? e.message : String(e)}`).toConvexError()
1489
- });
1490
- yield* Fx.from({
1491
- ok: () => {
1492
- validateEnterpriseSamlLoginRelayState({
1493
- relayState: parsedResponse.relayState,
1494
- source: {
1495
- kind: "enterprise",
1496
- id: enterprise._id
1497
- },
1498
- inResponseTo: parsedResponse.parsed.extract?.response?.inResponseTo
1499
- });
1500
- return Promise.resolve();
1501
- },
1502
- err: () => new AuthError("OAUTH_INVALID_STATE", "SAML RelayState did not match the pending login request.").toConvexError()
1503
- });
1504
- const { samlAttributes, samlSessionIndex, ...userProfile } = profileFromSamlExtract(parsedResponse.parsed.extract, saml.attributeMapping);
1505
- const profile = userProfile;
1506
- const maybeRedirectTo = useRedirectToParam(enterpriseSamlProviderId(enterprise._id), getCookies(request));
1507
- const verificationCode = yield* Fx.from({
1508
- ok: () => callUserOAuth(ctx, {
1509
- provider: enterpriseSamlProviderId(enterprise._id),
1510
- providerAccountId: profile.id,
1511
- profile,
1512
- signature: parsedResponse.relayState.signature,
1513
- accountExtend: {
1514
- identity: {
1515
- protocol: "saml",
1516
- enterpriseId: enterprise._id,
1517
- subject: profile.id,
1518
- entityId: typeof saml.entityId === "string" ? saml.entityId : void 0
1519
- },
1520
- saml: {
1521
- attributes: samlAttributes,
1522
- sessionIndex: samlSessionIndex
1523
- }
1524
- }
1525
- }),
1526
- err: (e) => e
1527
- });
1528
- const vurl = setURLSearchParam(yield* Fx.from({
1529
- ok: () => redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo ?? (typeof parsedResponse.relayState.redirectTo === "string" ? parsedResponse.relayState.redirectTo : void 0) }),
1530
- err: (e) => e
1531
- }), "code", verificationCode);
1532
- const vheaders = new Headers({ Location: vurl });
1533
- vheaders.set("Cache-Control", "must-revalidate");
1534
- for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) vheaders.append("Set-Cookie", serialize(name, value, options));
1535
- return new Response(null, {
1536
- status: 302,
1537
- headers: vheaders
1538
- });
1539
- }).pipe(Fx.recover((e) => Fx.fatal(e)))));
1540
- const samlSloHandler = convertErrorsToResponse(400, async (ctx, request) => {
1541
- const runtimePathname = new URL(request.url).pathname;
1542
- const runtimePrefix = `${ENTERPRISE_CONTROL_ROUTE_BASE}/`;
1543
- const [runtimeEnterpriseId, protocol, ...rest] = runtimePathname.startsWith(runtimePrefix) ? runtimePathname.slice(runtimePrefix.length).split("/").filter(Boolean) : [];
1544
- const runtimeRoute = runtimeEnterpriseId !== void 0 && (protocol === "oidc" || protocol === "saml" || protocol === "scim") && rest.length > 0 ? {
1545
- pathname: runtimePathname,
1546
- enterpriseId: runtimeEnterpriseId,
1547
- protocol,
1548
- rest
1549
- } : null;
1550
- if (!runtimeRoute || runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "slo") throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
1551
- const enterpriseId = runtimeRoute.enterpriseId;
1552
- const enterpriseDoc = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
1553
- if (enterpriseDoc === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
1554
- const loaded = {
1555
- source: {
1556
- kind: "enterprise",
1557
- id: enterpriseId
1558
- },
1559
- config: enterpriseDoc.config,
1560
- status: enterpriseDoc.status,
1561
- enterprise: enterpriseDoc
1562
- };
1563
- if (!isEnterpriseSamlSourceActive(loaded)) throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
1564
- if (!getSamlConfig(loaded.config).idp?.metadataXml) throw new AuthError("PROVIDER_NOT_CONFIGURED", "SAML is not configured for this enterprise.").toConvexError();
1565
- const enterprise = loaded.enterprise;
1566
- const parsedMessage = await parseEnterpriseSamlLogoutMessage({
1567
- request,
1568
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1569
- source: {
1570
- kind: "enterprise",
1571
- id: enterprise._id
1572
- },
1573
- config: loaded.config
1574
- });
1575
- if (parsedMessage.hasSamlRequest && parsedMessage.parsedRequest) {
1576
- const responseContext = parsedMessage.runtime.sp.createLogoutResponse(parsedMessage.runtime.idp, parsedMessage.parsedRequest.extract, parsedMessage.binding, parsedMessage.relayState ?? "");
1577
- if (parsedMessage.binding === "redirect") return new Response(null, {
1578
- status: 302,
1579
- headers: { Location: responseContext.context }
1580
- });
1581
- return createSamlPostBindingResponse({
1582
- endpoint: responseContext.entityEndpoint,
1583
- parameter: "SAMLResponse",
1584
- value: responseContext.context,
1585
- relayState: parsedMessage.relayState
1586
- });
1587
- }
1588
- if (parsedMessage.hasSamlResponse) return new Response(null, { status: 204 });
1589
- throw new AuthError("INVALID_PARAMETERS", "Missing SAML logout payload.").toConvexError();
1590
- });
1591
- const enterpriseScimHandler = async (ctx, request) => {
1592
- try {
1593
- const { scimConfig, enterprise, parsedPath } = await getEnterpriseScimContext(ctx, request);
1594
- const state = {
1595
- ctx,
1596
- request,
1597
- url: new URL(request.url),
1598
- parsedPath,
1599
- enterprise,
1600
- scimConfig,
1601
- recordScimEvent: async (eventType, ok, subjectType, subjectId, metadata) => {
1602
- const auditEventId = await recordEnterpriseAuditEvent(ctx, {
1603
- enterpriseId: enterprise._id,
1604
- groupId: enterprise.groupId,
1605
- eventType,
1606
- actorType: "scim",
1607
- subjectType,
1608
- subjectId,
1609
- ok,
1610
- metadata
1611
- });
1612
- await emitEnterpriseWebhookDeliveries(ctx, {
1613
- enterpriseId: enterprise._id,
1614
- eventType,
1615
- auditEventId,
1616
- payload: {
1617
- enterpriseId: enterprise._id,
1618
- subjectId,
1619
- metadata
1620
- }
1621
- });
1622
- }
1623
- };
1624
- const handleUsersGet = async (state$1) => {
1625
- const members = await auth.member.list(state$1.ctx, {
1626
- where: { groupId: state$1.enterprise.groupId },
1627
- limit: 100
1628
- });
1629
- const identities = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityListByEnterprise, { enterpriseId: state$1.enterprise._id });
1630
- const identityByUserId = new Map(identities.filter((identity) => identity.userId !== void 0).map((identity) => [identity.userId, identity]));
1631
- const users = (await Promise.all(members.items.map(async (member) => {
1632
- const user = await auth.user.get(state$1.ctx, member.userId);
1633
- return user ? {
1634
- user,
1635
- member,
1636
- identity: identityByUserId.get(user._id)
1637
- } : null;
1638
- }))).filter(Boolean);
1639
- const listRequest = parseScimListRequest(state$1.url);
1640
- const filtered = filterScimCollection(users, listRequest.filter, {
1641
- id: (item, value) => item.user._id === value,
1642
- externalId: (item, value) => item.identity?.externalId === value,
1643
- userName: (item, value) => item.user.email === value,
1644
- "emails.value": (item, value) => item.user.email === value,
1645
- active: (item, value) => String(item.identity?.active ?? item.member.status === "active") === value
1646
- });
1647
- if (state$1.parsedPath.resourceId) {
1648
- const resource = filtered.find(({ user }) => user._id === state$1.parsedPath.resourceId);
1649
- return resource ? scimJson(serializeScimUser({
1650
- id: resource.user._id,
1651
- user: resource.user,
1652
- externalId: resource.identity?.externalId,
1653
- location: `${state$1.url.origin}${state$1.url.pathname.replace(/\/[^/]+$/, "")}/${resource.user._id}`,
1654
- active: resource.identity?.active ?? resource.member.status === "active"
1655
- }), 200, { Location: `${state$1.url.origin}${state$1.url.pathname.replace(/\/[^/]+$/, "")}/${resource.user._id}` }) : scimError(404, "notFound", "User not found.");
1656
- }
1657
- const paged = paginateScimCollection(filtered, listRequest);
1658
- await state$1.recordScimEvent("enterprise.scim.read", true, "enterprise_scim", state$1.scimConfig._id);
1659
- return scimJson({
1660
- schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
1661
- Resources: paged.map(({ user, identity, member }) => serializeScimUser({
1662
- id: user._id,
1663
- user,
1664
- externalId: identity?.externalId,
1665
- location: `${state$1.url.origin}${state$1.url.pathname}/${user._id}`,
1666
- active: identity?.active ?? member.status === "active"
1667
- })),
1668
- totalResults: filtered.length,
1669
- startIndex: listRequest.startIndex,
1670
- itemsPerPage: paged.length
1671
- });
1672
- };
1673
- const handleUsersPost = async (state$1) => {
1674
- const body = await readScimJson(state$1.request);
1675
- const primaryEmail = Array.isArray(body.emails) ? body.emails.find((entry) => entry.primary === true)?.value ?? body.emails[0]?.value : void 0;
1676
- const phone = Array.isArray(body.phoneNumbers) ? body.phoneNumbers[0]?.value : void 0;
1677
- const userId = await state$1.ctx.runMutation(config.component.public.userInsert, { data: {
1678
- name: body.displayName ?? body.name?.formatted,
1679
- email: primaryEmail ?? body.userName,
1680
- ...typeof (primaryEmail ?? body.userName) === "string" ? { emailVerificationTime: Date.now() } : {},
1681
- phone,
1682
- ...typeof phone === "string" ? { phoneVerificationTime: Date.now() } : {}
1683
- } });
1684
- try {
1685
- await auth.member.add(state$1.ctx, {
1686
- groupId: state$1.enterprise.groupId,
1687
- userId,
1688
- role: "member",
1689
- status: body.active === false ? "inactive" : "active"
1690
- });
1691
- } catch {}
1692
- await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
1693
- enterpriseId: state$1.enterprise._id,
1694
- groupId: state$1.enterprise.groupId,
1695
- resourceType: "user",
1696
- externalId: typeof body.externalId === "string" ? body.externalId : void 0,
1697
- userId,
1698
- active: body.active !== false,
1699
- raw: body,
1700
- lastProvisionedAt: Date.now()
1701
- });
1702
- await state$1.recordScimEvent("enterprise.scim.user.created", true, "user", userId);
1703
- const createdUser = await auth.user.get(state$1.ctx, userId);
1704
- const location = `${state$1.url.origin}${state$1.url.pathname}/${userId}`;
1705
- return scimJson(serializeScimUser({
1706
- id: userId,
1707
- user: createdUser ?? {},
1708
- externalId: body.externalId,
1709
- location,
1710
- active: body.active !== false
1711
- }), 201, { Location: location });
1712
- };
1713
- const handleUsersUpsert = async (state$1) => {
1714
- const missing = requireScimResourceId(state$1.parsedPath.resourceId, "User");
1715
- if (missing) return missing;
1716
- const userId = state$1.parsedPath.resourceId;
1717
- const existingUser = await auth.user.get(state$1.ctx, userId);
1718
- if (!existingUser) return scimError(404, "notFound", "User not found.");
1719
- const body = await readScimJson(state$1.request);
1720
- const patchData = {};
1721
- let nextActive;
1722
- if (state$1.request.method === "PUT") {
1723
- patchData.name = body.displayName ?? body.name?.formatted;
1724
- patchData.email = body.userName ?? (Array.isArray(body.emails) ? body.emails[0]?.value : void 0);
1725
- patchData.phone = Array.isArray(body.phoneNumbers) ? body.phoneNumbers[0]?.value : void 0;
1726
- if (typeof patchData.email === "string") patchData.emailVerificationTime = Date.now();
1727
- if (typeof patchData.phone === "string") patchData.phoneVerificationTime = Date.now();
1728
- } else for (const operation of Array.isArray(body.Operations) ? body.Operations : []) {
1729
- if (operation.path === "active") nextActive = operation.value;
1730
- if (operation.path === "displayName" || operation.path === "name.formatted") patchData.name = operation.value;
1731
- if (operation.path === "userName" || operation.path === "emails.value") {
1732
- patchData.email = operation.value;
1733
- if (typeof operation.value === "string") patchData.emailVerificationTime = Date.now();
1734
- }
1735
- if (operation.path === "phoneNumbers.value") {
1736
- patchData.phone = operation.value;
1737
- if (typeof operation.value === "string") patchData.phoneVerificationTime = Date.now();
1738
- }
1739
- }
1740
- await state$1.ctx.runMutation(config.component.public.userPatch, {
1741
- userId,
1742
- data: patchData
1743
- });
1744
- const membership = await auth.member.getByUserAndGroup(state$1.ctx, {
1745
- groupId: state$1.enterprise.groupId,
1746
- userId
1747
- });
1748
- if (membership) await auth.member.update(state$1.ctx, membership._id, { status: body.active === false || nextActive === false ? "inactive" : "active" });
1749
- await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
1750
- enterpriseId: state$1.enterprise._id,
1751
- groupId: state$1.enterprise.groupId,
1752
- resourceType: "user",
1753
- externalId: typeof body.externalId === "string" ? body.externalId : void 0,
1754
- userId,
1755
- active: body.active !== false && nextActive !== false,
1756
- raw: body,
1757
- lastProvisionedAt: Date.now()
1758
- });
1759
- await state$1.recordScimEvent("enterprise.scim.user.updated", true, "user", userId);
1760
- const updatedUser = await auth.user.get(state$1.ctx, userId);
1761
- const location = `${state$1.url.origin}${state$1.url.pathname}`;
1762
- return scimJson(serializeScimUser({
1763
- id: userId,
1764
- user: updatedUser ?? existingUser,
1765
- externalId: typeof body.externalId === "string" ? body.externalId : void 0,
1766
- location,
1767
- active: body.active !== false && nextActive !== false
1768
- }), 200, { Location: location });
1769
- };
1770
- const handleUsersDelete = async (state$1) => {
1771
- const missing = requireScimResourceId(state$1.parsedPath.resourceId, "User");
1772
- if (missing) return missing;
1773
- const userId = state$1.parsedPath.resourceId;
1774
- const membership = await auth.member.getByUserAndGroup(state$1.ctx, {
1775
- groupId: state$1.enterprise.groupId,
1776
- userId
1777
- });
1778
- if (membership) await auth.member.remove(state$1.ctx, membership._id);
1779
- const identity = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityGetByUser, { userId });
1780
- if (identity) if (state$1.scimConfig.deprovisionMode === "hard") await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityDelete, { identityId: identity._id });
1781
- else await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
1782
- enterpriseId: identity.enterpriseId,
1783
- groupId: identity.groupId,
1784
- resourceType: identity.resourceType,
1785
- externalId: identity.externalId,
1786
- userId: identity.userId,
1787
- mappedGroupId: identity.mappedGroupId,
1788
- active: false,
1789
- raw: identity.raw,
1790
- lastProvisionedAt: Date.now()
1791
- });
1792
- await state$1.recordScimEvent("enterprise.scim.user.deleted", true, "user", userId);
1793
- return new Response(null, { status: 204 });
1794
- };
1795
- const handleGroupsGet = async (state$1) => {
1796
- const groupsList = await auth.group.list(state$1.ctx, {
1797
- where: { parentGroupId: state$1.enterprise.groupId },
1798
- limit: 100
1799
- });
1800
- const identities = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityListByEnterprise, { enterpriseId: state$1.enterprise._id });
1801
- const identityByGroupId = new Map(identities.filter((identity) => identity.mappedGroupId !== void 0).map((identity) => [identity.mappedGroupId, identity]));
1802
- const groups = groupsList.items.map((group) => ({
1803
- group,
1804
- identity: identityByGroupId.get(group._id)
1805
- }));
1806
- const listRequest = parseScimListRequest(state$1.url);
1807
- const filtered = filterScimCollection(groups, listRequest.filter, {
1808
- id: (item, value) => item.group._id === value,
1809
- externalId: (item, value) => item.identity?.externalId === value,
1810
- displayName: (item, value) => item.group.name === value
1811
- });
1812
- if (state$1.parsedPath.resourceId) {
1813
- const resource = filtered.find(({ group }) => group._id === state$1.parsedPath.resourceId);
1814
- if (!resource) return scimError(404, "notFound", "Group not found.");
1815
- const members = (await auth.member.list(state$1.ctx, {
1816
- where: {
1817
- groupId: resource.group._id,
1818
- status: "active"
1819
- },
1820
- limit: 100
1821
- })).items.map((member) => ({ value: member.userId }));
1822
- const location = `${state$1.url.origin}${state$1.url.pathname.replace(/\/[^/]+$/, "")}/${resource.group._id}`;
1823
- return scimJson(serializeScimGroup({
1824
- id: resource.group._id,
1825
- group: resource.group,
1826
- externalId: resource.identity?.externalId,
1827
- location,
1828
- members
1829
- }), 200, { Location: location });
1830
- }
1831
- const paged = paginateScimCollection(filtered, listRequest);
1832
- return scimJson({
1833
- schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
1834
- Resources: paged.map(({ group, identity }) => serializeScimGroup({
1835
- id: group._id,
1836
- group,
1837
- externalId: identity?.externalId,
1838
- location: `${state$1.url.origin}${state$1.url.pathname}/${group._id}`
1839
- })),
1840
- totalResults: filtered.length,
1841
- startIndex: listRequest.startIndex,
1842
- itemsPerPage: paged.length
1843
- });
1844
- };
1845
- const handleGroupsPost = async (state$1) => {
1846
- const body = await readScimJson(state$1.request);
1847
- const groupId = await auth.group.create(state$1.ctx, {
1848
- name: String(body.displayName ?? "Group"),
1849
- parentGroupId: state$1.enterprise.groupId,
1850
- type: "organization"
1851
- });
1852
- await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
1853
- enterpriseId: state$1.enterprise._id,
1854
- groupId: state$1.enterprise.groupId,
1855
- resourceType: "group",
1856
- externalId: body.externalId ?? groupId,
1857
- mappedGroupId: groupId,
1858
- active: true,
1859
- raw: body,
1860
- lastProvisionedAt: Date.now()
1861
- });
1862
- for (const member of Array.isArray(body.members) ? body.members : []) try {
1863
- await auth.member.add(state$1.ctx, {
1864
- groupId,
1865
- userId: String(member.value),
1866
- role: "member",
1867
- status: "active"
1868
- });
1869
- } catch {}
1870
- await state$1.recordScimEvent("enterprise.scim.group.created", true, "group", groupId);
1871
- const group = await auth.group.get(state$1.ctx, groupId);
1872
- const location = `${state$1.url.origin}${state$1.url.pathname}/${groupId}`;
1873
- return scimJson(serializeScimGroup({
1874
- id: groupId,
1875
- group: group ?? {},
1876
- externalId: body.externalId,
1877
- location,
1878
- members: (await auth.member.list(state$1.ctx, {
1879
- where: {
1880
- groupId,
1881
- status: "active"
1882
- },
1883
- limit: 100
1884
- })).items.map((member) => ({ value: member.userId }))
1885
- }), 201, { Location: location });
1886
- };
1887
- const handleGroupsPatch = async (state$1) => {
1888
- const missing = requireScimResourceId(state$1.parsedPath.resourceId, "Group");
1889
- if (missing) return missing;
1890
- const groupId = state$1.parsedPath.resourceId;
1891
- const body = await readScimJson(state$1.request);
1892
- for (const operation of Array.isArray(body.Operations) ? body.Operations : []) {
1893
- if (operation.path === "displayName") await auth.group.update(state$1.ctx, groupId, { name: operation.value });
1894
- if (operation.path === "members" && operation.op === "add") for (const member of Array.isArray(operation.value) ? operation.value : []) try {
1895
- await auth.member.add(state$1.ctx, {
1896
- groupId,
1897
- userId: String(member.value),
1898
- role: "member",
1899
- status: "active"
1900
- });
1901
- } catch {}
1902
- if (operation.path === "members" && operation.op === "replace") {
1903
- const currentMembers = (await auth.member.list(state$1.ctx, {
1904
- where: {
1905
- groupId,
1906
- status: "active"
1907
- },
1908
- limit: 100
1909
- })).items;
1910
- const currentUserIds = new Set(currentMembers.map((member) => member.userId));
1911
- const nextUserIds = new Set((Array.isArray(operation.value) ? operation.value : []).map((member) => String(member.value)));
1912
- for (const member of currentMembers) if (!nextUserIds.has(member.userId)) await auth.member.remove(state$1.ctx, member._id);
1913
- for (const userId of nextUserIds.values()) if (!currentUserIds.has(userId)) try {
1914
- await auth.member.add(state$1.ctx, {
1915
- groupId,
1916
- userId,
1917
- role: "member",
1918
- status: "active"
1919
- });
1920
- } catch {}
1921
- }
1922
- if (typeof operation.path === "string" && operation.op === "remove" && operation.path.startsWith("members[")) {
1923
- const userId = operation.path.match(/^members\[value eq "([^"]+)"\]$/)?.[1];
1924
- if (userId) {
1925
- const membership = await auth.member.getByUserAndGroup(state$1.ctx, {
1926
- groupId,
1927
- userId
1928
- });
1929
- if (membership) await auth.member.remove(state$1.ctx, membership._id);
1930
- }
1931
- }
1932
- }
1933
- await state$1.recordScimEvent("enterprise.scim.group.updated", true, "group", groupId);
1934
- const group = await auth.group.get(state$1.ctx, groupId);
1935
- const location = `${state$1.url.origin}${state$1.url.pathname}`;
1936
- const members = (await auth.member.list(state$1.ctx, {
1937
- where: {
1938
- groupId,
1939
- status: "active"
1940
- },
1941
- limit: 100
1942
- })).items;
1943
- return scimJson(serializeScimGroup({
1944
- id: groupId,
1945
- group: group ?? {},
1946
- location,
1947
- members: members.map((member) => ({ value: member.userId }))
1948
- }), 200, { Location: location });
1949
- };
1950
- const handleGroupsDelete = async (state$1) => {
1951
- const missing = requireScimResourceId(state$1.parsedPath.resourceId, "Group");
1952
- if (missing) return missing;
1953
- const groupId = state$1.parsedPath.resourceId;
1954
- await auth.group.delete(state$1.ctx, groupId);
1955
- const identity = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityGetByMappedGroup, { mappedGroupId: groupId });
1956
- if (identity) await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityDelete, { identityId: identity._id });
1957
- await state$1.recordScimEvent("enterprise.scim.group.deleted", true, "group", groupId);
1958
- return new Response(null, { status: 204 });
1959
- };
1960
- const handler = {
1961
- ServiceProviderConfig: { GET: async () => scimJson({
1962
- schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
1963
- patch: { supported: true },
1964
- bulk: {
1965
- supported: false,
1966
- maxOperations: 0,
1967
- maxPayloadSize: 0
1968
- },
1969
- filter: {
1970
- supported: true,
1971
- maxResults: 100
1972
- },
1973
- changePassword: { supported: false },
1974
- sort: { supported: false },
1975
- etag: { supported: false },
1976
- authenticationSchemes: [{
1977
- type: "oauthbearertoken",
1978
- name: "Bearer Token",
1979
- description: "Use the SCIM token generated by Convex Auth enterprise."
1980
- }]
1981
- }) },
1982
- Schemas: { GET: async (state$1) => handleStaticScimCollection(SCIM_SCHEMAS, state$1.parsedPath.resourceId, {
1983
- by: "id",
1984
- notFound: "Schema not found."
1985
- }) },
1986
- ResourceTypes: { GET: async (state$1) => handleStaticScimCollection(SCIM_RESOURCE_TYPES, state$1.parsedPath.resourceId, {
1987
- by: "name",
1988
- notFound: "Resource type not found."
1989
- }) },
1990
- Users: {
1991
- GET: handleUsersGet,
1992
- POST: handleUsersPost,
1993
- PATCH: handleUsersUpsert,
1994
- PUT: handleUsersUpsert,
1995
- DELETE: handleUsersDelete
1996
- },
1997
- Groups: {
1998
- GET: handleGroupsGet,
1999
- POST: handleGroupsPost,
2000
- PATCH: handleGroupsPatch,
2001
- DELETE: handleGroupsDelete
2002
- }
2003
- }[state.parsedPath.resource]?.[state.request.method];
2004
- return handler ? await handler(state) : scimError(404, "notFound", "SCIM resource not found.");
2005
- } catch (error) {
2006
- if (error instanceof Error && error.message === "Unsupported SCIM filter.") return scimError(400, "invalidFilter", error.message);
2007
- if (isAuthError(error)) {
2008
- const code = error.data.code;
2009
- return scimError(code === "MISSING_BEARER_TOKEN" || code === "INVALID_API_KEY" ? 401 : 400, code, error.data.message);
2010
- }
2011
- throw error;
2012
- }
2013
- };
2014
- for (const method of ["PATCH", "DELETE"]) http.route({
2015
- pathPrefix: "/api/auth/sso/",
2016
- method,
2017
- handler: httpActionGeneric(async (ctx, request) => {
2018
- const runtimePathname = new URL(request.url).pathname;
2019
- const runtimePrefix = `${ENTERPRISE_CONTROL_ROUTE_BASE}/`;
2020
- const [runtimeEnterpriseId, protocol, ...rest] = runtimePathname.startsWith(runtimePrefix) ? runtimePathname.slice(runtimePrefix.length).split("/").filter(Boolean) : [];
2021
- const runtimeRoute = runtimeEnterpriseId !== void 0 && (protocol === "oidc" || protocol === "saml" || protocol === "scim") && rest.length > 0 ? {
2022
- pathname: runtimePathname,
2023
- enterpriseId: runtimeEnterpriseId,
2024
- protocol,
2025
- rest
2026
- } : null;
2027
- if (!runtimeRoute || runtimeRoute.protocol !== "scim" || runtimeRoute.rest[0] !== "v2") return scimError(404, "notFound", "SCIM resource not found.");
2028
- return await enterpriseScimHandler(ctx, request);
2029
- })
2030
- });
2031
- }
2032
- if (hasOAuth) {
2033
- http.route({
2034
- pathPrefix: "/api/auth/signin/",
2035
- method: "GET",
2036
- handler: httpActionGeneric(convertErrorsToResponse(400, async (ctx, request) => {
2037
- const url = new URL(request.url);
2038
- const providerId = url.pathname.split("/").at(-1);
2039
- if (providerId === null) throw new AuthError("OAUTH_MISSING_PROVIDER").toConvexError();
2040
- const verifier = url.searchParams.get("code");
2041
- if (verifier === null) throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
2042
- const oauthConfig = getProviderOrThrow(providerId);
2043
- const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig.provider, oauthConfig);
2044
- await callVerifierSignature(ctx, {
2045
- verifier,
2046
- signature
2047
- });
2048
- const redirectTo = url.searchParams.get("redirectTo");
2049
- if (redirectTo !== null) cookies.push(redirectToParamCookie(providerId, redirectTo));
2050
- const headers = new Headers({ Location: redirect });
2051
- for (const { name, value, options } of cookies) headers.append("Set-Cookie", serialize(name, value, options));
2052
- return new Response(null, {
2053
- status: 302,
2054
- headers
2055
- });
2056
- }))
2057
- });
2058
- const callbackAction = httpActionGeneric(async (ctx, request) => {
2059
- const url = new URL(request.url);
2060
- const providerId = new URL(request.url).pathname.split("/").at(-1);
2061
- if (!providerId) throw new AuthError("OAUTH_MISSING_PROVIDER").toConvexError();
2062
- logWithLevel(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
2063
- const provider = getProviderOrThrow(providerId);
2064
- const cookies = getCookies(request);
2065
- const maybeRedirectTo = useRedirectToParam(provider.id, cookies);
2066
- const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
2067
- const params = url.searchParams;
2068
- if (request.headers.get("Content-Type") === "application/x-www-form-urlencoded") (await request.formData()).forEach((value, key) => {
2069
- if (typeof value === "string") params.append(key, value);
2070
- });
2071
- return Fx.run(Fx.from({
2072
- ok: async () => {
2073
- const oauthConfig = provider;
2074
- const result = await Fx.run(handleOAuthCallback(providerId, oauthConfig.provider, oauthConfig, Object.fromEntries(params.entries()), cookies));
2075
- const oauthCookies = result.cookies;
2076
- const { id: profileId, ...profileData } = result.profile;
2077
- const { signature } = result;
2078
- const redirUrl = setURLSearchParam(destinationUrl, "code", await callUserOAuth(ctx, {
2079
- provider: providerId,
2080
- providerAccountId: profileId,
2081
- profile: profileData,
2082
- signature
2083
- }));
2084
- const redirHeaders = new Headers({ Location: redirUrl });
2085
- redirHeaders.set("Cache-Control", "must-revalidate");
2086
- for (const { name, value, options } of [...oauthCookies, ...maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []]) redirHeaders.append("Set-Cookie", serialize(name, value, options));
2087
- return new Response(null, {
2088
- status: 302,
2089
- headers: redirHeaders
2090
- });
2091
- },
2092
- err: (error) => error
2093
- }).pipe(Fx.recover((error) => {
2094
- logError(error);
2095
- const respHeaders = new Headers({ Location: destinationUrl });
2096
- for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) respHeaders.append("Set-Cookie", serialize(name, value, options));
2097
- return Fx.succeed(new Response(null, {
2098
- status: 302,
2099
- headers: respHeaders
2100
- }));
2101
- })));
2102
- });
2103
- http.route({
2104
- pathPrefix: "/api/auth/callback/",
2105
- method: "GET",
2106
- handler: callbackAction
2107
- });
2108
- http.route({
2109
- pathPrefix: "/api/auth/callback/",
2110
- method: "POST",
2111
- handler: callbackAction
2112
- });
2113
- }
2114
- },
2115
- action: (handler, options) => {
2116
- const corsConfig = options?.cors ?? {};
2117
- const corsHeaders = {
2118
- "Access-Control-Allow-Origin": corsConfig.origin ?? "*",
2119
- "Access-Control-Allow-Methods": corsConfig.methods ?? "GET,POST,PUT,PATCH,DELETE,OPTIONS",
2120
- "Access-Control-Allow-Headers": corsConfig.headers ?? "Content-Type,Authorization"
2121
- };
2122
- return httpActionGeneric(async (genericCtx, request) => {
2123
- return Fx.run(Fx.from({
2124
- ok: async () => {
2125
- const authHeader = request.headers.get("Authorization");
2126
- if (!authHeader?.startsWith("Bearer ")) return new Response(JSON.stringify({
2127
- error: "Missing or malformed Authorization: Bearer header.",
2128
- code: "MISSING_BEARER_TOKEN"
2129
- }), {
2130
- status: 401,
2131
- headers: {
2132
- ...corsHeaders,
2133
- "Content-Type": "application/json"
2134
- }
2135
- });
2136
- const rawKey = authHeader.slice(7);
2137
- const keyResult = await Fx.run(Fx.from({
2138
- ok: () => auth.key.verify(genericCtx, rawKey),
2139
- err: (error) => error
2140
- }).pipe(Fx.fold({
2141
- ok: (result$1) => ({
2142
- ok: true,
2143
- value: result$1
2144
- }),
2145
- err: (error) => ({
2146
- ok: false,
2147
- error
2148
- })
2149
- })));
2150
- if (!keyResult.ok) {
2151
- if (isAuthError(keyResult.error)) {
2152
- const { code, message } = keyResult.error.data;
2153
- return new Response(JSON.stringify({
2154
- error: message,
2155
- code
2156
- }), {
2157
- status: 403,
2158
- headers: {
2159
- ...corsHeaders,
2160
- "Content-Type": "application/json"
2161
- }
2162
- });
2163
- }
2164
- throw keyResult.error;
2165
- }
2166
- if (options?.scope) {
2167
- if (!keyResult.value.scopes.can(options.scope.resource, options.scope.action)) return new Response(JSON.stringify({
2168
- error: "This API key does not have the required permissions.",
2169
- code: "SCOPE_CHECK_FAILED"
2170
- }), {
2171
- status: 403,
2172
- headers: {
2173
- ...corsHeaders,
2174
- "Content-Type": "application/json"
2175
- }
2176
- });
2177
- }
2178
- const result = await handler(Object.assign(genericCtx, { key: {
2179
- userId: keyResult.value.userId,
2180
- keyId: keyResult.value.keyId,
2181
- scopes: keyResult.value.scopes
2182
- } }), request);
2183
- if (result instanceof Response) {
2184
- const headers = new Headers(result.headers);
2185
- for (const [k, val] of Object.entries(corsHeaders)) if (!headers.has(k)) headers.set(k, val);
2186
- return new Response(result.body, {
2187
- status: result.status,
2188
- statusText: result.statusText,
2189
- headers
2190
- });
2191
- }
2192
- return new Response(JSON.stringify(result), {
2193
- status: 200,
2194
- headers: {
2195
- ...corsHeaders,
2196
- "Content-Type": "application/json"
2197
- }
2198
- });
2199
- },
2200
- err: (error) => error
2201
- }).pipe(Fx.recover((error) => {
2202
- logError(error);
2203
- return Fx.succeed(new Response(JSON.stringify({
2204
- error: "An unexpected error occurred.",
2205
- code: "INTERNAL_ERROR"
2206
- }), {
2207
- status: 500,
2208
- headers: {
2209
- ...corsHeaders,
2210
- "Content-Type": "application/json"
2211
- }
2212
- }));
2213
- })));
2214
- });
2215
- },
2216
- route: (http, routeConfig) => {
2217
- const corsConfig = routeConfig.cors ?? {};
2218
- const corsHeaders = {
2219
- "Access-Control-Allow-Origin": corsConfig.origin ?? "*",
2220
- "Access-Control-Allow-Methods": corsConfig.methods ?? "GET,POST,PUT,PATCH,DELETE,OPTIONS",
2221
- "Access-Control-Allow-Headers": corsConfig.headers ?? "Content-Type,Authorization"
2222
- };
2223
- http.route({
2224
- path: routeConfig.path,
2225
- method: "OPTIONS",
2226
- handler: httpActionGeneric(async () => {
2227
- return new Response(null, {
2228
- status: 204,
2229
- headers: corsHeaders
2230
- });
2231
- })
2232
- });
2233
- http.route({
2234
- path: routeConfig.path,
2235
- method: routeConfig.method,
2236
- handler: auth.http.action(routeConfig.handler, {
2237
- scope: routeConfig.scope,
2238
- cors: routeConfig.cors
2239
- })
2240
- });
2241
- }
2242
- }
2243
- };
2244
- const enrichCtx = (ctx) => ({
2245
- ...ctx,
2246
- auth: {
2247
- ...ctx.auth,
2248
- config,
2249
- account: auth.account,
2250
- session: auth.session,
2251
- provider: auth.provider
2252
- }
2253
- });
2254
- return {
2255
- auth,
2256
- signIn: actionGeneric({
2257
- args: {
2258
- provider: v.optional(v.string()),
2259
- params: v.optional(v.any()),
2260
- verifier: v.optional(v.string()),
2261
- refreshToken: v.optional(v.string()),
2262
- calledBy: v.optional(v.string())
2263
- },
2264
- handler: async (ctx, args) => {
2265
- if (args.calledBy !== void 0) logWithLevel("INFO", `\`auth/session:start\` called by ${args.calledBy}`);
2266
- const provider = args.provider !== void 0 ? getProviderOrThrow(args.provider) : null;
2267
- const result = await signInImpl(enrichCtx(ctx), provider, args, {
2268
- generateTokens: true,
2269
- allowExtraProviders: false
2270
- });
2271
- return Fx.run(Fx.match(result, result.kind, {
2272
- redirect: (r) => Fx.succeed({
2273
- kind: "redirect",
2274
- redirect: r.redirect,
2275
- verifier: r.verifier
2276
- }),
2277
- signedIn: (r) => Fx.succeed({
2278
- kind: "signedIn",
2279
- tokens: r.signedIn?.tokens ?? null
2280
- }),
2281
- refreshTokens: (r) => Fx.succeed({
2282
- kind: "signedIn",
2283
- tokens: r.signedIn?.tokens ?? null
2284
- }),
2285
- started: () => Fx.succeed({ kind: "started" }),
2286
- passkeyOptions: (r) => Fx.succeed({
2287
- kind: "passkeyOptions",
2288
- options: r.options,
2289
- verifier: r.verifier
2290
- }),
2291
- totpRequired: (r) => Fx.succeed({
2292
- kind: "totpRequired",
2293
- verifier: r.verifier
2294
- }),
2295
- totpSetup: (r) => Fx.succeed({
2296
- kind: "totpSetup",
2297
- totpSetup: {
2298
- uri: r.uri,
2299
- secret: r.secret,
2300
- totpId: r.totpId
2301
- },
2302
- verifier: r.verifier
2303
- }),
2304
- deviceCode: (r) => Fx.succeed({
2305
- kind: "deviceCode",
2306
- deviceCode: {
2307
- deviceCode: r.deviceCode,
2308
- userCode: r.userCode,
2309
- verificationUri: r.verificationUri,
2310
- verificationUriComplete: r.verificationUriComplete,
2311
- expiresIn: r.expiresIn,
2312
- interval: r.interval
2313
- }
2314
- })
2315
- }));
2316
- }
2317
- }),
2318
- signOut: actionGeneric({
2319
- args: {},
2320
- handler: async (ctx) => {
2321
- await callSignOut(ctx);
2322
- }
2323
- }),
2324
- store: internalMutationGeneric({
2325
- args: storeArgs,
2326
- handler: async (ctx, args) => {
2327
- return storeImpl(ctx, args, getProviderOrThrow, config);
2328
- }
2329
- })
2330
- };
2331
- }
2332
- function convertErrorsToResponse(errorStatusCode, action) {
2333
- return async (ctx, request) => {
2334
- return Fx.run(Fx.from({
2335
- ok: () => action(ctx, request),
2336
- err: (error) => error
2337
- }).pipe(Fx.recover((error) => {
2338
- if (isAuthError(error)) return Fx.succeed(new Response(JSON.stringify({
2339
- code: error.data.code,
2340
- message: error.data.message
2341
- }), {
2342
- status: errorStatusCode,
2343
- headers: { "Content-Type": "application/json" }
2344
- }));
2345
- else if (error instanceof ConvexError) return Fx.succeed(new Response(null, {
2346
- status: errorStatusCode,
2347
- statusText: typeof error.data === "string" ? error.data : "Error"
2348
- }));
2349
- else {
2350
- logError(error);
2351
- return Fx.succeed(new Response(null, {
2352
- status: 500,
2353
- statusText: "Internal Server Error"
2354
- }));
2355
- }
2356
- })));
2357
- };
2358
- }
2359
- function getCookies(request) {
2360
- return parse(request.headers.get("Cookie") ?? "");
2361
- }
2362
-
2363
- //#endregion
2364
- export { Auth };
2365
- //# sourceMappingURL=implementation.js.map