@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.
- package/README.md +140 -9
- package/dist/bin.cjs +5957 -5478
- package/dist/client/index.d.ts +3 -7
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +27 -26
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +14 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +1672 -24
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/index.d.ts +1 -1
- package/dist/component/index.js +2 -2
- package/dist/component/model.d.ts +153 -0
- package/dist/component/model.d.ts.map +1 -0
- package/dist/component/model.js +343 -0
- package/dist/component/model.js.map +1 -0
- package/dist/component/providers/sso.d.ts +1 -1
- package/dist/component/public/enterprise.d.ts +54 -0
- package/dist/component/public/enterprise.d.ts.map +1 -0
- package/dist/component/public/enterprise.js +515 -0
- package/dist/component/public/enterprise.js.map +1 -0
- package/dist/component/public/factors.d.ts +52 -0
- package/dist/component/public/factors.d.ts.map +1 -0
- package/dist/component/public/factors.js +285 -0
- package/dist/component/public/factors.js.map +1 -0
- package/dist/component/public/groups.d.ts +116 -0
- package/dist/component/public/groups.d.ts.map +1 -0
- package/dist/component/public/groups.js +596 -0
- package/dist/component/public/groups.js.map +1 -0
- package/dist/component/public/identity.d.ts +93 -0
- package/dist/component/public/identity.d.ts.map +1 -0
- package/dist/component/public/identity.js +426 -0
- package/dist/component/public/identity.js.map +1 -0
- package/dist/component/public/keys.d.ts +41 -0
- package/dist/component/public/keys.d.ts.map +1 -0
- package/dist/component/public/keys.js +157 -0
- package/dist/component/public/keys.js.map +1 -0
- package/dist/component/public/shared.d.ts +26 -0
- package/dist/component/public/shared.d.ts.map +1 -0
- package/dist/component/public/shared.js +32 -0
- package/dist/component/public/shared.js.map +1 -0
- package/dist/component/public.d.ts +9 -321
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +6 -2145
- package/dist/component/schema.d.ts +406 -260
- package/dist/component/schema.js +37 -32
- package/dist/component/schema.js.map +1 -1
- package/dist/component/server/auth.d.ts +161 -15
- package/dist/component/server/auth.d.ts.map +1 -1
- package/dist/component/server/auth.js +100 -7
- package/dist/component/server/auth.js.map +1 -1
- package/dist/component/server/cookies.js +3 -0
- package/dist/component/server/cookies.js.map +1 -1
- package/dist/component/server/db.js +1 -0
- package/dist/component/server/db.js.map +1 -1
- package/dist/component/server/device.js +3 -1
- package/dist/component/server/device.js.map +1 -1
- package/dist/component/server/domains/core.js +629 -0
- package/dist/component/server/domains/core.js.map +1 -0
- package/dist/component/server/domains/sso.js +884 -0
- package/dist/component/server/domains/sso.js.map +1 -0
- package/dist/component/server/factory.d.ts +136 -0
- package/dist/component/server/factory.d.ts.map +1 -0
- package/dist/component/server/factory.js +1134 -0
- package/dist/component/server/factory.js.map +1 -0
- package/dist/component/server/fx.js +2 -1
- package/dist/component/server/fx.js.map +1 -1
- package/dist/component/server/http.js +287 -0
- package/dist/component/server/http.js.map +1 -0
- package/dist/component/server/identity.js +13 -0
- package/dist/component/server/identity.js.map +1 -0
- package/dist/component/server/keys.js +4 -0
- package/dist/component/server/keys.js.map +1 -1
- package/dist/component/server/mutations/account.js +1 -1
- package/dist/component/server/mutations/index.js +2 -2
- package/dist/component/server/mutations/index.js.map +1 -1
- package/dist/component/server/mutations/invalidate.js +1 -1
- package/dist/component/server/mutations/oauth.js +10 -7
- package/dist/component/server/mutations/oauth.js.map +1 -1
- package/dist/component/server/mutations/refresh.js +1 -1
- package/dist/component/server/mutations/register.js +1 -1
- package/dist/component/server/mutations/retrieve.js +1 -1
- package/dist/component/server/mutations/signature.js +1 -1
- package/dist/component/server/mutations/store.js +6 -3
- package/dist/component/server/mutations/store.js.map +1 -1
- package/dist/component/server/mutations/verify.js +1 -1
- package/dist/component/server/oauth.js +3 -0
- package/dist/component/server/oauth.js.map +1 -1
- package/dist/component/server/passkey.js +3 -2
- package/dist/component/server/passkey.js.map +1 -1
- package/dist/component/server/provider.js +2 -0
- package/dist/component/server/provider.js.map +1 -1
- package/dist/component/server/providers.js +10 -0
- package/dist/component/server/providers.js.map +1 -1
- package/dist/component/server/ratelimit.js +3 -0
- package/dist/component/server/ratelimit.js.map +1 -1
- package/dist/component/server/redirects.js +2 -0
- package/dist/component/server/redirects.js.map +1 -1
- package/dist/component/server/refresh.js +5 -0
- package/dist/component/server/refresh.js.map +1 -1
- package/dist/component/server/sessions.js +5 -0
- package/dist/component/server/sessions.js.map +1 -1
- package/dist/component/server/signin.js +2 -1
- package/dist/component/server/signin.js.map +1 -1
- package/dist/component/server/sso.js +166 -19
- package/dist/component/server/sso.js.map +1 -1
- package/dist/component/server/tokens.js +1 -0
- package/dist/component/server/tokens.js.map +1 -1
- package/dist/component/server/totp.js +4 -2
- package/dist/component/server/totp.js.map +1 -1
- package/dist/component/server/types.d.ts +106 -38
- package/dist/component/server/types.d.ts.map +1 -1
- package/dist/component/server/types.js.map +1 -1
- package/dist/component/server/users.js +1 -0
- package/dist/component/server/users.js.map +1 -1
- package/dist/component/server/utils.js +44 -2
- package/dist/component/server/utils.js.map +1 -1
- package/dist/providers/anonymous.d.ts +1 -1
- package/dist/providers/credentials.d.ts +1 -1
- package/dist/providers/password.d.ts +1 -1
- package/dist/providers/sso.d.ts +1 -1
- package/dist/providers/sso.js.map +1 -1
- package/dist/server/auth.d.ts +163 -17
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +100 -7
- package/dist/server/auth.js.map +1 -1
- package/dist/server/cookies.d.ts +1 -38
- package/dist/server/cookies.js +3 -0
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/db.d.ts +1 -125
- package/dist/server/db.js +1 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/device.d.ts +1 -24
- package/dist/server/device.js +3 -1
- package/dist/server/device.js.map +1 -1
- package/dist/server/domains/core.d.ts +434 -0
- package/dist/server/domains/core.d.ts.map +1 -0
- package/dist/server/domains/core.js +629 -0
- package/dist/server/domains/core.js.map +1 -0
- package/dist/server/domains/sso.d.ts +409 -0
- package/dist/server/domains/sso.d.ts.map +1 -0
- package/dist/server/domains/sso.js +884 -0
- package/dist/server/domains/sso.js.map +1 -0
- package/dist/server/enterpriseValidators.d.ts +1 -0
- package/dist/server/enterpriseValidators.js +60 -0
- package/dist/server/enterpriseValidators.js.map +1 -0
- package/dist/server/factory.d.ts +136 -0
- package/dist/server/factory.d.ts.map +1 -0
- package/dist/server/factory.js +1134 -0
- package/dist/server/factory.js.map +1 -0
- package/dist/server/fx.d.ts +1 -16
- package/dist/server/fx.d.ts.map +1 -1
- package/dist/server/fx.js +1 -0
- package/dist/server/fx.js.map +1 -1
- package/dist/server/http.d.ts +59 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +287 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/identity.d.ts +1 -0
- package/dist/server/identity.js +13 -0
- package/dist/server/identity.js.map +1 -0
- package/dist/server/index.d.ts +468 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +530 -36
- package/dist/server/index.js.map +1 -1
- package/dist/server/keys.d.ts +1 -57
- package/dist/server/keys.js +4 -0
- package/dist/server/keys.js.map +1 -1
- package/dist/server/mutations/account.d.ts +7 -7
- package/dist/server/mutations/account.d.ts.map +1 -1
- package/dist/server/mutations/code.d.ts +13 -13
- package/dist/server/mutations/code.d.ts.map +1 -1
- package/dist/server/mutations/index.d.ts +107 -107
- package/dist/server/mutations/index.d.ts.map +1 -1
- package/dist/server/mutations/index.js +1 -1
- package/dist/server/mutations/index.js.map +1 -1
- package/dist/server/mutations/invalidate.d.ts +5 -5
- package/dist/server/mutations/invalidate.d.ts.map +1 -1
- package/dist/server/mutations/oauth.d.ts +10 -10
- package/dist/server/mutations/oauth.d.ts.map +1 -1
- package/dist/server/mutations/oauth.js +9 -6
- package/dist/server/mutations/oauth.js.map +1 -1
- package/dist/server/mutations/refresh.d.ts +4 -4
- package/dist/server/mutations/register.d.ts +12 -12
- package/dist/server/mutations/register.d.ts.map +1 -1
- package/dist/server/mutations/retrieve.d.ts +7 -7
- package/dist/server/mutations/signature.d.ts +5 -5
- package/dist/server/mutations/signin.d.ts +6 -6
- package/dist/server/mutations/signin.d.ts.map +1 -1
- package/dist/server/mutations/signout.d.ts +1 -1
- package/dist/server/mutations/store.d.ts +3 -2
- package/dist/server/mutations/store.d.ts.map +1 -1
- package/dist/server/mutations/store.js +6 -3
- package/dist/server/mutations/store.js.map +1 -1
- package/dist/server/mutations/verifier.d.ts +1 -1
- package/dist/server/mutations/verify.d.ts +11 -11
- package/dist/server/mutations/verify.d.ts.map +1 -1
- package/dist/server/oauth.d.ts +1 -59
- package/dist/server/oauth.js +3 -0
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/passkey.d.ts.map +1 -1
- package/dist/server/passkey.js +3 -2
- package/dist/server/passkey.js.map +1 -1
- package/dist/server/provider.d.ts +1 -14
- package/dist/server/provider.d.ts.map +1 -1
- package/dist/server/provider.js +2 -0
- package/dist/server/provider.js.map +1 -1
- package/dist/server/providers.js +10 -0
- package/dist/server/providers.js.map +1 -1
- package/dist/server/ratelimit.d.ts +1 -22
- package/dist/server/ratelimit.js +3 -0
- package/dist/server/ratelimit.js.map +1 -1
- package/dist/server/redirects.d.ts +1 -10
- package/dist/server/redirects.js +2 -0
- package/dist/server/redirects.js.map +1 -1
- package/dist/server/refresh.d.ts +1 -37
- package/dist/server/refresh.js +5 -0
- package/dist/server/refresh.js.map +1 -1
- package/dist/server/sessions.d.ts +1 -28
- package/dist/server/sessions.js +5 -0
- package/dist/server/sessions.js.map +1 -1
- package/dist/server/signin.d.ts +1 -55
- package/dist/server/signin.js +2 -1
- package/dist/server/signin.js.map +1 -1
- package/dist/server/sso.d.ts +1 -348
- package/dist/server/sso.js +165 -18
- package/dist/server/sso.js.map +1 -1
- package/dist/server/templates.d.ts +1 -21
- package/dist/server/templates.js +1 -0
- package/dist/server/templates.js.map +1 -1
- package/dist/server/tokens.d.ts +1 -11
- package/dist/server/tokens.js +1 -0
- package/dist/server/tokens.js.map +1 -1
- package/dist/server/totp.d.ts +1 -23
- package/dist/server/totp.js +4 -2
- package/dist/server/totp.js.map +1 -1
- package/dist/server/types.d.ts +114 -77
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/server/users.d.ts +1 -31
- package/dist/server/users.js +1 -0
- package/dist/server/users.js.map +1 -1
- package/dist/server/utils.d.ts +1 -27
- package/dist/server/utils.js +44 -2
- package/dist/server/utils.js.map +1 -1
- package/dist/server/version.d.ts +1 -1
- package/dist/server/version.js +1 -1
- package/dist/server/version.js.map +1 -1
- package/package.json +4 -5
- package/src/cli/bin.ts +5 -0
- package/src/cli/index.ts +22 -9
- package/src/cli/keys.ts +3 -0
- package/src/client/index.ts +36 -37
- package/src/component/_generated/api.ts +14 -0
- package/src/component/_generated/component.ts +2106 -9
- package/src/component/index.ts +3 -1
- package/src/component/model.ts +441 -0
- package/src/component/public/enterprise.ts +753 -0
- package/src/component/public/factors.ts +332 -0
- package/src/component/public/groups.ts +932 -0
- package/src/component/public/identity.ts +566 -0
- package/src/component/public/keys.ts +209 -0
- package/src/component/public/shared.ts +119 -0
- package/src/component/public.ts +5 -2965
- package/src/component/schema.ts +68 -63
- package/src/providers/sso.ts +1 -1
- package/src/server/auth.ts +413 -18
- package/src/server/cookies.ts +3 -0
- package/src/server/db.ts +3 -0
- package/src/server/device.ts +3 -1
- package/src/server/domains/core.ts +1071 -0
- package/src/server/domains/sso.ts +1749 -0
- package/src/server/enterpriseValidators.ts +93 -0
- package/src/server/factory.ts +2181 -0
- package/src/server/fx.ts +1 -0
- package/src/server/http.ts +529 -0
- package/src/server/identity.ts +18 -0
- package/src/server/index.ts +806 -40
- package/src/server/keys.ts +4 -0
- package/src/server/mutations/index.ts +1 -1
- package/src/server/mutations/oauth.ts +36 -8
- package/src/server/mutations/store.ts +6 -3
- package/src/server/oauth.ts +6 -0
- package/src/server/passkey.ts +3 -2
- package/src/server/provider.ts +2 -0
- package/src/server/providers.ts +20 -0
- package/src/server/ratelimit.ts +3 -0
- package/src/server/redirects.ts +2 -0
- package/src/server/refresh.ts +5 -0
- package/src/server/sessions.ts +5 -0
- package/src/server/signin.ts +1 -0
- package/src/server/sso.ts +259 -17
- package/src/server/templates.ts +1 -0
- package/src/server/tokens.ts +1 -0
- package/src/server/totp.ts +4 -2
- package/src/server/types.ts +178 -83
- package/src/server/users.ts +1 -0
- package/src/server/utils.ts +71 -1
- package/src/server/version.ts +1 -1
- package/dist/component/public.js.map +0 -1
- package/dist/component/server/implementation.d.ts +0 -1264
- package/dist/component/server/implementation.d.ts.map +0 -1
- package/dist/component/server/implementation.js +0 -2365
- package/dist/component/server/implementation.js.map +0 -1
- package/dist/server/cookies.d.ts.map +0 -1
- package/dist/server/db.d.ts.map +0 -1
- package/dist/server/device.d.ts.map +0 -1
- package/dist/server/implementation.d.ts +0 -1264
- package/dist/server/implementation.d.ts.map +0 -1
- package/dist/server/implementation.js +0 -2365
- package/dist/server/implementation.js.map +0 -1
- package/dist/server/keys.d.ts.map +0 -1
- package/dist/server/oauth.d.ts.map +0 -1
- package/dist/server/ratelimit.d.ts.map +0 -1
- package/dist/server/redirects.d.ts.map +0 -1
- package/dist/server/refresh.d.ts.map +0 -1
- package/dist/server/sessions.d.ts.map +0 -1
- package/dist/server/signin.d.ts.map +0 -1
- package/dist/server/sso.d.ts.map +0 -1
- package/dist/server/templates.d.ts.map +0 -1
- package/dist/server/tokens.d.ts.map +0 -1
- package/dist/server/totp.d.ts.map +0 -1
- package/dist/server/users.d.ts.map +0 -1
- package/dist/server/utils.d.ts.map +0 -1
- 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 { ConvexError, v } from "convex/values";
|
|
20
|
-
import { actionGeneric, httpActionGeneric, internalMutationGeneric } from "convex/server";
|
|
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
|