@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
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { AuthError, Fx } from "../fx.js";
|
|
2
|
+
|
|
3
|
+
//#region src/server/domains/sso.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the enterprise and SSO management domain.
|
|
6
|
+
*/
|
|
7
|
+
function createSsoDomain(deps) {
|
|
8
|
+
const { config, normalizeEnterprisePolicy, normalizeDomain, getEnterpriseSecret, loadEnterpriseOrThrow, validateEnterprisePolicy, recordEnterpriseAuditEvent, emitEnterpriseWebhookDeliveries, enterpriseNotFoundError, ENTERPRISE_OIDC_CLIENT_SECRET_KIND, requireEnv, generateRandomString, INVITE_TOKEN_ALPHABET, sha256, encryptSecret, upsertProtocolConfig, parseSamlIdpMetadata, createServiceProviderMetadata, getSamlServiceProviderOptions, getPublicOidcConfig, withOidcSecretState, getOidcConfig, getEnterpriseOidcUrls, enterpriseOidcProviderId, getPolicyFromEnterprise, patchEnterprisePolicy } = deps;
|
|
9
|
+
const ENTERPRISE_DOMAIN_VERIFICATION_PREFIX = "_convex-auth-verification";
|
|
10
|
+
const ENTERPRISE_DOMAIN_VERIFICATION_TTL_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
11
|
+
const toDomainSummary = (domain) => ({
|
|
12
|
+
domainId: domain._id,
|
|
13
|
+
domain: domain.domain,
|
|
14
|
+
isPrimary: domain.isPrimary,
|
|
15
|
+
verified: domain.verifiedAt !== void 0,
|
|
16
|
+
verifiedAt: domain.verifiedAt ?? null
|
|
17
|
+
});
|
|
18
|
+
const getDomainVerificationRecordName = (domain) => `${ENTERPRISE_DOMAIN_VERIFICATION_PREFIX}.${normalizeDomain(domain)}`;
|
|
19
|
+
const parseTxtAnswer = (value) => {
|
|
20
|
+
const quoted = [...value.matchAll(/"([^"]*)"/g)].map((match) => match[1]);
|
|
21
|
+
if (quoted.length > 0) return quoted.join("");
|
|
22
|
+
return value.replace(/^"|"$/g, "").trim();
|
|
23
|
+
};
|
|
24
|
+
const resolveTxtValues = async (recordName) => {
|
|
25
|
+
const url = new URL("https://dns.google/resolve");
|
|
26
|
+
url.searchParams.set("name", recordName);
|
|
27
|
+
url.searchParams.set("type", "TXT");
|
|
28
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
29
|
+
if (!response.ok) throw new Error(`DNS TXT lookup failed with status ${response.status}.`);
|
|
30
|
+
return ((await response.json()).Answer ?? []).map((answer) => typeof answer.data === "string" ? parseTxtAnswer(answer.data) : null).filter((value) => value !== null && value.length > 0);
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
connection: {
|
|
34
|
+
create: async (ctx, data) => {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
enterpriseId: await ctx.runMutation(config.component.public.enterpriseCreate, {
|
|
38
|
+
...data,
|
|
39
|
+
policy: normalizeEnterprisePolicy(data.policy)
|
|
40
|
+
}),
|
|
41
|
+
groupId: data.groupId
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
get: async (ctx, enterpriseId) => {
|
|
45
|
+
return await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
46
|
+
},
|
|
47
|
+
getByGroup: async (ctx, groupId) => {
|
|
48
|
+
return await ctx.runQuery(config.component.public.enterpriseGetByGroup, { groupId });
|
|
49
|
+
},
|
|
50
|
+
getByDomain: async (ctx, domain) => {
|
|
51
|
+
return await ctx.runQuery(config.component.public.enterpriseGetByDomain, { domain: normalizeDomain(domain) });
|
|
52
|
+
},
|
|
53
|
+
list: async (ctx, opts) => {
|
|
54
|
+
return await ctx.runQuery(config.component.public.enterpriseList, {
|
|
55
|
+
where: opts?.where,
|
|
56
|
+
limit: opts?.limit,
|
|
57
|
+
cursor: opts?.cursor,
|
|
58
|
+
orderBy: opts?.orderBy,
|
|
59
|
+
order: opts?.order
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
update: async (ctx, enterpriseId, data) => {
|
|
63
|
+
await ctx.runMutation(config.component.public.enterpriseUpdate, {
|
|
64
|
+
enterpriseId,
|
|
65
|
+
data
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
enterpriseId
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
delete: async (ctx, enterpriseId) => {
|
|
73
|
+
await ctx.runMutation(config.component.public.enterpriseDelete, { enterpriseId });
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
enterpriseId
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
status: async (ctx, enterpriseId) => {
|
|
80
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
81
|
+
if (!enterprise) throw new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError).toConvexError();
|
|
82
|
+
const policy = getPolicyFromEnterprise(enterprise);
|
|
83
|
+
const protocols = enterprise.config?.protocols ?? {};
|
|
84
|
+
const oidcConfig = protocols.oidc;
|
|
85
|
+
const oidcSecret = await getEnterpriseSecret(ctx, enterprise._id, ENTERPRISE_OIDC_CLIENT_SECRET_KIND);
|
|
86
|
+
const samlConfig = protocols.saml;
|
|
87
|
+
const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByEnterprise, { enterpriseId });
|
|
88
|
+
const domains = await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId });
|
|
89
|
+
const oidcReady = oidcConfig?.enabled === true && typeof oidcConfig?.clientId === "string" && oidcConfig.clientId.length > 0 && oidcSecret !== null && (typeof oidcConfig?.issuer === "string" || typeof oidcConfig?.discoveryUrl === "string");
|
|
90
|
+
const samlReady = samlConfig?.enabled === true && typeof samlConfig?.idp?.entityId === "string";
|
|
91
|
+
const scimReady = scimConfig !== null && scimConfig !== void 0 && scimConfig.status === "active";
|
|
92
|
+
const ready = enterprise.status === "active" && (oidcReady || samlReady);
|
|
93
|
+
return {
|
|
94
|
+
enterpriseId: enterprise._id,
|
|
95
|
+
status: enterprise.status,
|
|
96
|
+
ready,
|
|
97
|
+
domainCount: domains.length,
|
|
98
|
+
protocols: {
|
|
99
|
+
oidc: {
|
|
100
|
+
configured: oidcReady,
|
|
101
|
+
ready: oidcReady,
|
|
102
|
+
clientId: oidcConfig?.clientId ?? null,
|
|
103
|
+
issuer: oidcConfig?.issuer ?? oidcConfig?.discoveryUrl ?? null
|
|
104
|
+
},
|
|
105
|
+
saml: {
|
|
106
|
+
configured: samlReady,
|
|
107
|
+
ready: samlReady,
|
|
108
|
+
entityId: samlConfig?.idp?.entityId ?? null
|
|
109
|
+
},
|
|
110
|
+
scim: {
|
|
111
|
+
configured: scimReady,
|
|
112
|
+
ready: scimReady,
|
|
113
|
+
basePath: scimConfig?.basePath ?? null,
|
|
114
|
+
deprovisionMode: policy.provisioning.deprovision.mode
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
domain: {
|
|
121
|
+
add: async (ctx, data) => {
|
|
122
|
+
return await ctx.runMutation(config.component.public.enterpriseDomainAdd, {
|
|
123
|
+
...data,
|
|
124
|
+
domain: normalizeDomain(data.domain)
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
list: async (ctx, enterpriseId) => {
|
|
128
|
+
return await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId });
|
|
129
|
+
},
|
|
130
|
+
validate: async (ctx, enterpriseId) => {
|
|
131
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
132
|
+
if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError).toConvexError();
|
|
133
|
+
const domains = await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId });
|
|
134
|
+
const primaryDomains = domains.filter((domain) => domain.isPrimary);
|
|
135
|
+
const verifiedDomains = domains.filter((domain) => domain.verifiedAt !== void 0);
|
|
136
|
+
const warnings = [];
|
|
137
|
+
if (domains.length === 0) warnings.push("No domains configured.");
|
|
138
|
+
if (primaryDomains.length === 0 && domains.length > 0) warnings.push("No primary domain configured.");
|
|
139
|
+
if (primaryDomains.length > 1) warnings.push("Multiple primary domains configured.");
|
|
140
|
+
if (verifiedDomains.length === 0 && domains.length > 0) warnings.push("No verified domains yet.");
|
|
141
|
+
return {
|
|
142
|
+
enterpriseId,
|
|
143
|
+
ready: enterprise.status === "active" && domains.length > 0 && primaryDomains.length === 1 && verifiedDomains.length > 0,
|
|
144
|
+
summary: {
|
|
145
|
+
domainCount: domains.length,
|
|
146
|
+
primaryCount: primaryDomains.length,
|
|
147
|
+
verifiedCount: verifiedDomains.length
|
|
148
|
+
},
|
|
149
|
+
domains: domains.map((domain) => toDomainSummary(domain)),
|
|
150
|
+
warnings
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
remove: async (ctx, domainId) => {
|
|
154
|
+
await ctx.runMutation(config.component.public.enterpriseDomainDelete, { domainId });
|
|
155
|
+
},
|
|
156
|
+
verification: {
|
|
157
|
+
request: async (ctx, args) => {
|
|
158
|
+
const enterprise = await loadEnterpriseOrThrow(ctx, args.enterpriseId);
|
|
159
|
+
const normalizedDomain = normalizeDomain(args.domain);
|
|
160
|
+
const domain = (await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId: enterprise._id })).find((entry) => entry.domain === normalizedDomain);
|
|
161
|
+
if (!domain) throw new AuthError("INVALID_PARAMETERS", "Domain is not attached to this enterprise.").toConvexError();
|
|
162
|
+
const requestedAt = Date.now();
|
|
163
|
+
const expiresAt = requestedAt + ENTERPRISE_DOMAIN_VERIFICATION_TTL_MS;
|
|
164
|
+
const token = generateRandomString(32, INVITE_TOKEN_ALPHABET);
|
|
165
|
+
const tokenHash = await sha256(token);
|
|
166
|
+
const recordName = getDomainVerificationRecordName(normalizedDomain);
|
|
167
|
+
await ctx.runMutation(config.component.public.enterpriseDomainVerificationUpsert, {
|
|
168
|
+
enterpriseId: enterprise._id,
|
|
169
|
+
groupId: enterprise.groupId,
|
|
170
|
+
domainId: domain._id,
|
|
171
|
+
domain: normalizedDomain,
|
|
172
|
+
recordName,
|
|
173
|
+
token,
|
|
174
|
+
tokenHash,
|
|
175
|
+
requestedAt,
|
|
176
|
+
expiresAt
|
|
177
|
+
});
|
|
178
|
+
await recordEnterpriseAuditEvent(ctx, {
|
|
179
|
+
enterpriseId: enterprise._id,
|
|
180
|
+
groupId: enterprise.groupId,
|
|
181
|
+
eventType: "enterprise.domain.verification_requested",
|
|
182
|
+
actorType: "system",
|
|
183
|
+
subjectType: "enterprise_domain",
|
|
184
|
+
subjectId: domain._id,
|
|
185
|
+
ok: true,
|
|
186
|
+
metadata: {
|
|
187
|
+
domain: normalizedDomain,
|
|
188
|
+
recordName,
|
|
189
|
+
expiresAt
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
enterpriseId: enterprise._id,
|
|
195
|
+
domain: normalizedDomain,
|
|
196
|
+
requestedAt,
|
|
197
|
+
expiresAt,
|
|
198
|
+
challenge: {
|
|
199
|
+
recordType: "TXT",
|
|
200
|
+
recordName,
|
|
201
|
+
recordValue: token
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
confirm: async (ctx, args) => {
|
|
206
|
+
const enterprise = await loadEnterpriseOrThrow(ctx, args.enterpriseId);
|
|
207
|
+
const normalizedDomain = normalizeDomain(args.domain);
|
|
208
|
+
const domain = (await ctx.runQuery(config.component.public.enterpriseDomainList, { enterpriseId: enterprise._id })).find((entry) => entry.domain === normalizedDomain);
|
|
209
|
+
if (!domain) throw new AuthError("INVALID_PARAMETERS", "Domain is not attached to this enterprise.").toConvexError();
|
|
210
|
+
if (domain.verifiedAt !== void 0) return {
|
|
211
|
+
ok: true,
|
|
212
|
+
enterpriseId: enterprise._id,
|
|
213
|
+
domain: normalizedDomain,
|
|
214
|
+
verifiedAt: domain.verifiedAt,
|
|
215
|
+
checks: [{
|
|
216
|
+
name: "domain_verified",
|
|
217
|
+
ok: true,
|
|
218
|
+
message: "Domain is already verified."
|
|
219
|
+
}]
|
|
220
|
+
};
|
|
221
|
+
const verification = await ctx.runQuery(config.component.public.enterpriseDomainVerificationGet, { domainId: domain._id });
|
|
222
|
+
const checks = [];
|
|
223
|
+
if (!verification) {
|
|
224
|
+
checks.push({
|
|
225
|
+
name: "verification_requested",
|
|
226
|
+
ok: false,
|
|
227
|
+
message: "No active domain verification challenge exists."
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
enterpriseId: enterprise._id,
|
|
232
|
+
domain: normalizedDomain,
|
|
233
|
+
checks
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
checks.push({
|
|
237
|
+
name: "verification_requested",
|
|
238
|
+
ok: true
|
|
239
|
+
});
|
|
240
|
+
if (verification.expiresAt < Date.now()) {
|
|
241
|
+
await ctx.runMutation(config.component.public.enterpriseDomainVerificationDelete, { domainId: domain._id });
|
|
242
|
+
checks.push({
|
|
243
|
+
name: "challenge_active",
|
|
244
|
+
ok: false,
|
|
245
|
+
message: "The verification challenge expired. Request a new one."
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
enterpriseId: enterprise._id,
|
|
250
|
+
domain: normalizedDomain,
|
|
251
|
+
checks
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
checks.push({
|
|
255
|
+
name: "challenge_active",
|
|
256
|
+
ok: true
|
|
257
|
+
});
|
|
258
|
+
let txtValues;
|
|
259
|
+
try {
|
|
260
|
+
txtValues = await resolveTxtValues(verification.recordName);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new AuthError("INTERNAL_ERROR", error instanceof Error ? error.message : "Failed to resolve DNS TXT records.").toConvexError();
|
|
263
|
+
}
|
|
264
|
+
checks.push({
|
|
265
|
+
name: "dns_record_present",
|
|
266
|
+
ok: txtValues.length > 0,
|
|
267
|
+
message: txtValues.length > 0 ? void 0 : `No TXT records found at ${verification.recordName}.`
|
|
268
|
+
});
|
|
269
|
+
const matches = txtValues.includes(verification.token);
|
|
270
|
+
checks.push({
|
|
271
|
+
name: "dns_record_matches",
|
|
272
|
+
ok: matches,
|
|
273
|
+
message: matches ? void 0 : `TXT record at ${verification.recordName} does not match the expected value.`
|
|
274
|
+
});
|
|
275
|
+
if (!checks.every((check) => check.ok)) return {
|
|
276
|
+
ok: false,
|
|
277
|
+
enterpriseId: enterprise._id,
|
|
278
|
+
domain: normalizedDomain,
|
|
279
|
+
checks
|
|
280
|
+
};
|
|
281
|
+
const verifiedAt = Date.now();
|
|
282
|
+
await ctx.runMutation(config.component.public.enterpriseDomainVerify, {
|
|
283
|
+
domainId: domain._id,
|
|
284
|
+
verifiedAt
|
|
285
|
+
});
|
|
286
|
+
await recordEnterpriseAuditEvent(ctx, {
|
|
287
|
+
enterpriseId: enterprise._id,
|
|
288
|
+
groupId: enterprise.groupId,
|
|
289
|
+
eventType: "enterprise.domain.verified",
|
|
290
|
+
actorType: "system",
|
|
291
|
+
subjectType: "enterprise_domain",
|
|
292
|
+
subjectId: domain._id,
|
|
293
|
+
ok: true,
|
|
294
|
+
metadata: {
|
|
295
|
+
domain: normalizedDomain,
|
|
296
|
+
verifiedAt
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
enterpriseId: enterprise._id,
|
|
302
|
+
domain: normalizedDomain,
|
|
303
|
+
verifiedAt,
|
|
304
|
+
checks
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
saml: {
|
|
310
|
+
configure: async (ctx, data) => {
|
|
311
|
+
return await Fx.run(Fx.gen(function* () {
|
|
312
|
+
const enterprise = yield* Fx.from({
|
|
313
|
+
ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId }),
|
|
314
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
|
|
315
|
+
}).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent)));
|
|
316
|
+
const metadataXml = yield* data.metadataXml ? Fx.succeed(data.metadataXml) : data.metadataUrl ? Fx.defer(() => Fx.from({
|
|
317
|
+
ok: async () => {
|
|
318
|
+
const response = await fetch(data.metadataUrl);
|
|
319
|
+
if (!response.ok) throw new Error(`Failed to fetch SAML metadata: ${response.status}`);
|
|
320
|
+
return await response.text();
|
|
321
|
+
},
|
|
322
|
+
err: (error) => new AuthError("INVALID_PARAMETERS", error instanceof Error ? error.message : "Failed to fetch SAML metadata")
|
|
323
|
+
})).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."));
|
|
324
|
+
const parsed = yield* Fx.from({
|
|
325
|
+
ok: () => parseSamlIdpMetadata(metadataXml),
|
|
326
|
+
err: () => new AuthError("INVALID_PARAMETERS", "Failed to parse SAML metadata.")
|
|
327
|
+
});
|
|
328
|
+
const baseConfig = upsertProtocolConfig(enterprise.config, "saml", {
|
|
329
|
+
enabled: true,
|
|
330
|
+
idp: {
|
|
331
|
+
metadataXml,
|
|
332
|
+
...parsed
|
|
333
|
+
},
|
|
334
|
+
sp: data.sp,
|
|
335
|
+
signAuthnRequests: data.signAuthnRequests ?? parsed.wantsSignedAuthnRequests,
|
|
336
|
+
attributeMapping: data.attributeMapping
|
|
337
|
+
});
|
|
338
|
+
const normalizedDomains = data.domains?.map(normalizeDomain);
|
|
339
|
+
const nextConfig = normalizedDomains ? {
|
|
340
|
+
...baseConfig,
|
|
341
|
+
domains: normalizedDomains
|
|
342
|
+
} : baseConfig;
|
|
343
|
+
yield* Fx.from({
|
|
344
|
+
ok: () => ctx.runMutation(config.component.public.enterpriseUpdate, {
|
|
345
|
+
enterpriseId: enterprise._id,
|
|
346
|
+
data: {
|
|
347
|
+
status: "active",
|
|
348
|
+
config: nextConfig
|
|
349
|
+
}
|
|
350
|
+
}),
|
|
351
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to persist SAML registration.")
|
|
352
|
+
});
|
|
353
|
+
if (normalizedDomains) for (const [index, domain] of normalizedDomains.entries()) yield* Fx.from({
|
|
354
|
+
ok: () => ctx.runMutation(config.component.public.enterpriseDomainAdd, {
|
|
355
|
+
enterpriseId: enterprise._id,
|
|
356
|
+
groupId: enterprise.groupId,
|
|
357
|
+
domain,
|
|
358
|
+
isPrimary: index === 0
|
|
359
|
+
}),
|
|
360
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to persist enterprise domain.")
|
|
361
|
+
});
|
|
362
|
+
yield* Fx.from({
|
|
363
|
+
ok: () => recordEnterpriseAuditEvent(ctx, {
|
|
364
|
+
enterpriseId: enterprise._id,
|
|
365
|
+
groupId: enterprise.groupId,
|
|
366
|
+
eventType: "enterprise.saml.registered",
|
|
367
|
+
actorType: "system",
|
|
368
|
+
subjectType: "enterprise_saml",
|
|
369
|
+
subjectId: enterprise._id,
|
|
370
|
+
ok: true,
|
|
371
|
+
metadata: {
|
|
372
|
+
metadataUrl: data.metadataUrl,
|
|
373
|
+
domains: normalizedDomains
|
|
374
|
+
}
|
|
375
|
+
}),
|
|
376
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to record SAML registration audit event.")
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
enterpriseId: enterprise._id,
|
|
381
|
+
groupId: enterprise.groupId
|
|
382
|
+
};
|
|
383
|
+
}).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
|
|
384
|
+
},
|
|
385
|
+
metadata: async (ctx, opts) => {
|
|
386
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: opts.enterpriseId });
|
|
387
|
+
if (!enterprise) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
|
|
388
|
+
return createServiceProviderMetadata(getSamlServiceProviderOptions({
|
|
389
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
390
|
+
source: {
|
|
391
|
+
kind: "enterprise",
|
|
392
|
+
id: enterprise._id
|
|
393
|
+
},
|
|
394
|
+
config: enterprise.config,
|
|
395
|
+
overrides: {
|
|
396
|
+
entityId: opts.entityId,
|
|
397
|
+
acsUrl: opts.acsUrl,
|
|
398
|
+
sloUrl: opts.sloUrl
|
|
399
|
+
}
|
|
400
|
+
}));
|
|
401
|
+
},
|
|
402
|
+
validate: async (ctx, enterpriseId) => {
|
|
403
|
+
const checks = [];
|
|
404
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
405
|
+
if (!enterprise) return {
|
|
406
|
+
ok: false,
|
|
407
|
+
enterpriseId,
|
|
408
|
+
checks: [{
|
|
409
|
+
name: "enterprise_exists",
|
|
410
|
+
ok: false,
|
|
411
|
+
message: "Enterprise not found."
|
|
412
|
+
}]
|
|
413
|
+
};
|
|
414
|
+
const samlConfig = enterprise.config?.protocols?.saml;
|
|
415
|
+
const samlConfigured = samlConfig?.enabled === true && typeof samlConfig?.idp?.metadataXml === "string";
|
|
416
|
+
checks.push({
|
|
417
|
+
name: "saml_configured",
|
|
418
|
+
ok: samlConfigured,
|
|
419
|
+
message: samlConfigured ? void 0 : "SAML is not configured."
|
|
420
|
+
});
|
|
421
|
+
const hasIdpMetadata = typeof samlConfig?.idp?.metadataXml === "string" && samlConfig.idp.metadataXml.length > 0;
|
|
422
|
+
checks.push({
|
|
423
|
+
name: "idp_metadata_present",
|
|
424
|
+
ok: hasIdpMetadata,
|
|
425
|
+
message: hasIdpMetadata ? void 0 : "IdP metadata XML is missing."
|
|
426
|
+
});
|
|
427
|
+
const hasEntityId = typeof samlConfig?.idp?.entityId === "string" && samlConfig.idp.entityId.length > 0;
|
|
428
|
+
checks.push({
|
|
429
|
+
name: "idp_entity_id",
|
|
430
|
+
ok: hasEntityId,
|
|
431
|
+
message: hasEntityId ? void 0 : "IdP entityId could not be parsed from metadata."
|
|
432
|
+
});
|
|
433
|
+
let spMetadataOk = false;
|
|
434
|
+
let spMetadataMessage;
|
|
435
|
+
if (samlConfigured) try {
|
|
436
|
+
createServiceProviderMetadata(getSamlServiceProviderOptions({
|
|
437
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
438
|
+
source: {
|
|
439
|
+
kind: "enterprise",
|
|
440
|
+
id: enterprise._id
|
|
441
|
+
},
|
|
442
|
+
config: enterprise.config,
|
|
443
|
+
overrides: {}
|
|
444
|
+
}));
|
|
445
|
+
spMetadataOk = true;
|
|
446
|
+
} catch (e) {
|
|
447
|
+
spMetadataMessage = e instanceof Error ? e.message : "SP metadata generation failed.";
|
|
448
|
+
}
|
|
449
|
+
else spMetadataMessage = "Skipped — SAML not configured.";
|
|
450
|
+
checks.push({
|
|
451
|
+
name: "sp_metadata_generates",
|
|
452
|
+
ok: spMetadataOk,
|
|
453
|
+
message: spMetadataMessage
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
ok: checks.every((c) => c.ok),
|
|
457
|
+
enterpriseId: enterprise._id,
|
|
458
|
+
checks
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
policy: {
|
|
463
|
+
get: async (ctx, enterpriseId) => {
|
|
464
|
+
return getPolicyFromEnterprise(await loadEnterpriseOrThrow(ctx, enterpriseId));
|
|
465
|
+
},
|
|
466
|
+
update: async (ctx, enterpriseId, patch) => {
|
|
467
|
+
const enterprise = await loadEnterpriseOrThrow(ctx, enterpriseId);
|
|
468
|
+
const policy = patchEnterprisePolicy(enterprise.policy, patch);
|
|
469
|
+
await ctx.runMutation(config.component.public.enterpriseUpdate, {
|
|
470
|
+
enterpriseId,
|
|
471
|
+
data: { policy }
|
|
472
|
+
});
|
|
473
|
+
await recordEnterpriseAuditEvent(ctx, {
|
|
474
|
+
enterpriseId: enterprise._id,
|
|
475
|
+
groupId: enterprise.groupId,
|
|
476
|
+
eventType: "enterprise.policy.updated",
|
|
477
|
+
actorType: "system",
|
|
478
|
+
subjectType: "enterprise_policy",
|
|
479
|
+
subjectId: enterprise._id,
|
|
480
|
+
ok: true,
|
|
481
|
+
metadata: { version: policy.version }
|
|
482
|
+
});
|
|
483
|
+
return policy;
|
|
484
|
+
},
|
|
485
|
+
validate: async (ctx, enterpriseId) => {
|
|
486
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
487
|
+
if (!enterprise) return {
|
|
488
|
+
ok: false,
|
|
489
|
+
enterpriseId,
|
|
490
|
+
checks: [{
|
|
491
|
+
name: "enterprise_exists",
|
|
492
|
+
ok: false,
|
|
493
|
+
message: enterpriseNotFoundError
|
|
494
|
+
}]
|
|
495
|
+
};
|
|
496
|
+
const policy = getPolicyFromEnterprise(enterprise);
|
|
497
|
+
const checks = validateEnterprisePolicy(policy);
|
|
498
|
+
return {
|
|
499
|
+
ok: checks.every((check) => check.ok),
|
|
500
|
+
enterpriseId,
|
|
501
|
+
policy,
|
|
502
|
+
checks
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
oidc: {
|
|
507
|
+
configure: async (ctx, data) => {
|
|
508
|
+
return await Fx.run(Fx.gen(function* () {
|
|
509
|
+
yield* Fx.guard(data.issuer === void 0 && data.discoveryUrl === void 0, Fx.fail(new AuthError("INVALID_PARAMETERS", "OIDC registration requires issuer or discoveryUrl.")));
|
|
510
|
+
const enterprise = yield* Fx.from({
|
|
511
|
+
ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId }),
|
|
512
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
|
|
513
|
+
}).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent)));
|
|
514
|
+
const nextConfig = upsertProtocolConfig(enterprise.config, "oidc", {
|
|
515
|
+
enabled: true,
|
|
516
|
+
issuer: data.issuer,
|
|
517
|
+
discoveryUrl: data.discoveryUrl,
|
|
518
|
+
clientId: data.clientId,
|
|
519
|
+
scopes: data.scopes ?? [
|
|
520
|
+
"openid",
|
|
521
|
+
"profile",
|
|
522
|
+
"email"
|
|
523
|
+
],
|
|
524
|
+
authorizationParams: data.authorizationParams,
|
|
525
|
+
clockToleranceSeconds: data.clockToleranceSeconds,
|
|
526
|
+
strictIssuer: data.strictIssuer,
|
|
527
|
+
extraFields: data.extraFields
|
|
528
|
+
});
|
|
529
|
+
yield* Fx.from({
|
|
530
|
+
ok: () => ctx.runMutation(config.component.public.enterpriseUpdate, {
|
|
531
|
+
enterpriseId: data.enterpriseId,
|
|
532
|
+
data: { config: nextConfig }
|
|
533
|
+
}),
|
|
534
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to persist OIDC registration.")
|
|
535
|
+
});
|
|
536
|
+
if (data.clientSecret !== void 0) {
|
|
537
|
+
const ciphertext = yield* Fx.from({
|
|
538
|
+
ok: () => encryptSecret(data.clientSecret),
|
|
539
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to encrypt OIDC client secret.")
|
|
540
|
+
});
|
|
541
|
+
yield* Fx.from({
|
|
542
|
+
ok: () => ctx.runMutation(config.component.public.enterpriseSecretUpsert, {
|
|
543
|
+
enterpriseId: data.enterpriseId,
|
|
544
|
+
groupId: enterprise.groupId,
|
|
545
|
+
kind: ENTERPRISE_OIDC_CLIENT_SECRET_KIND,
|
|
546
|
+
ciphertext,
|
|
547
|
+
updatedAt: Date.now()
|
|
548
|
+
}),
|
|
549
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to persist OIDC client secret.")
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
yield* Fx.from({
|
|
553
|
+
ok: () => recordEnterpriseAuditEvent(ctx, {
|
|
554
|
+
enterpriseId: data.enterpriseId,
|
|
555
|
+
groupId: enterprise.groupId,
|
|
556
|
+
eventType: "enterprise.oidc.registered",
|
|
557
|
+
actorType: "system",
|
|
558
|
+
subjectType: "enterprise_oidc",
|
|
559
|
+
subjectId: data.enterpriseId,
|
|
560
|
+
ok: true,
|
|
561
|
+
metadata: {
|
|
562
|
+
issuer: data.issuer,
|
|
563
|
+
discoveryUrl: data.discoveryUrl
|
|
564
|
+
}
|
|
565
|
+
}),
|
|
566
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to record OIDC registration audit event.")
|
|
567
|
+
});
|
|
568
|
+
const secret = yield* Fx.from({
|
|
569
|
+
ok: () => getEnterpriseSecret(ctx, data.enterpriseId, ENTERPRISE_OIDC_CLIENT_SECRET_KIND),
|
|
570
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to load OIDC secret metadata.")
|
|
571
|
+
});
|
|
572
|
+
return withOidcSecretState(getPublicOidcConfig(nextConfig), secret !== null);
|
|
573
|
+
}).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
|
|
574
|
+
},
|
|
575
|
+
get: async (ctx, enterpriseId) => {
|
|
576
|
+
return await Fx.run(Fx.from({
|
|
577
|
+
ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId }),
|
|
578
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
|
|
579
|
+
}).pipe(Fx.chain((ent) => ent === null ? Fx.fail(new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError)) : Fx.succeed(ent)), Fx.chain((enterprise) => Fx.from({
|
|
580
|
+
ok: async () => {
|
|
581
|
+
const secret = await getEnterpriseSecret(ctx, enterprise._id, ENTERPRISE_OIDC_CLIENT_SECRET_KIND);
|
|
582
|
+
return withOidcSecretState(getPublicOidcConfig(enterprise.config), secret !== null);
|
|
583
|
+
},
|
|
584
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to load OIDC secret metadata.")
|
|
585
|
+
})), Fx.recover((e) => Fx.fatal(e.toConvexError()))));
|
|
586
|
+
},
|
|
587
|
+
signIn: async (ctx, data) => {
|
|
588
|
+
return await Fx.run(Fx.gen(function* () {
|
|
589
|
+
const enterprise = data.enterpriseId !== void 0 ? yield* Fx.from({
|
|
590
|
+
ok: () => ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId }),
|
|
591
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to load enterprise.")
|
|
592
|
+
}).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({
|
|
593
|
+
ok: () => ctx.runQuery(config.component.public.enterpriseGetByDomain, { domain: normalizeDomain(data.domain ?? String(data.email).split("@").at(-1) ?? "") }),
|
|
594
|
+
err: () => new AuthError("INTERNAL_ERROR", "Failed to resolve enterprise by domain.")
|
|
595
|
+
}).pipe(Fx.chain((result) => result?.enterprise && result.domain?.verifiedAt !== void 0 ? 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."));
|
|
596
|
+
yield* Fx.guard(enterprise.status !== "active", Fx.fail(new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.")));
|
|
597
|
+
const oidc = getOidcConfig(enterprise.config);
|
|
598
|
+
yield* Fx.guard(oidc.enabled !== true, Fx.fail(new AuthError("PROVIDER_NOT_CONFIGURED", "OIDC is not configured for this enterprise.")));
|
|
599
|
+
const urls = getEnterpriseOidcUrls({
|
|
600
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
601
|
+
enterpriseId: enterprise._id
|
|
602
|
+
});
|
|
603
|
+
return {
|
|
604
|
+
enterpriseId: enterprise._id,
|
|
605
|
+
providerId: enterpriseOidcProviderId(enterprise._id),
|
|
606
|
+
signInPath: urls.signInUrl,
|
|
607
|
+
callbackPath: urls.callbackUrl,
|
|
608
|
+
redirectTo: data.redirectTo
|
|
609
|
+
};
|
|
610
|
+
}).pipe(Fx.recover((e) => Fx.fatal(e.toConvexError()))));
|
|
611
|
+
},
|
|
612
|
+
validate: async (ctx, enterpriseId) => {
|
|
613
|
+
const checks = [];
|
|
614
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
615
|
+
if (!enterprise) return {
|
|
616
|
+
ok: false,
|
|
617
|
+
enterpriseId,
|
|
618
|
+
checks: [{
|
|
619
|
+
name: "enterprise_exists",
|
|
620
|
+
ok: false,
|
|
621
|
+
message: "Enterprise not found."
|
|
622
|
+
}]
|
|
623
|
+
};
|
|
624
|
+
const oidc = getOidcConfig(enterprise.config);
|
|
625
|
+
const secret = await getEnterpriseSecret(ctx, enterprise._id, ENTERPRISE_OIDC_CLIENT_SECRET_KIND);
|
|
626
|
+
const oidcConfigured = oidc.enabled === true && typeof oidc.clientId === "string" && oidc.clientId.length > 0;
|
|
627
|
+
checks.push({
|
|
628
|
+
name: "oidc_configured",
|
|
629
|
+
ok: oidcConfigured,
|
|
630
|
+
message: oidcConfigured ? void 0 : "OIDC is not configured."
|
|
631
|
+
});
|
|
632
|
+
const hasClientId = typeof oidc.clientId === "string" && oidc.clientId.length > 0;
|
|
633
|
+
checks.push({
|
|
634
|
+
name: "client_id_present",
|
|
635
|
+
ok: hasClientId,
|
|
636
|
+
message: hasClientId ? void 0 : "clientId is missing."
|
|
637
|
+
});
|
|
638
|
+
checks.push({
|
|
639
|
+
name: "client_secret_stored",
|
|
640
|
+
ok: secret !== null,
|
|
641
|
+
message: secret !== null ? void 0 : "OIDC client secret is missing."
|
|
642
|
+
});
|
|
643
|
+
const discoveryTarget = oidc.discoveryUrl ?? oidc.issuer;
|
|
644
|
+
const hasDiscovery = typeof discoveryTarget === "string" && discoveryTarget.length > 0;
|
|
645
|
+
checks.push({
|
|
646
|
+
name: "issuer_or_discovery_url_present",
|
|
647
|
+
ok: hasDiscovery,
|
|
648
|
+
message: hasDiscovery ? void 0 : "issuer or discoveryUrl is missing."
|
|
649
|
+
});
|
|
650
|
+
let discoveryOk = false;
|
|
651
|
+
let discoveryMessage;
|
|
652
|
+
if (hasDiscovery) {
|
|
653
|
+
const discoveryUrl = oidc.discoveryUrl?.length ? oidc.discoveryUrl : `${oidc.issuer}/.well-known/openid-configuration`;
|
|
654
|
+
try {
|
|
655
|
+
const res = await fetch(discoveryUrl, {
|
|
656
|
+
headers: { Accept: "application/json" },
|
|
657
|
+
signal: AbortSignal.timeout(8e3)
|
|
658
|
+
});
|
|
659
|
+
if (!res.ok) discoveryMessage = `Discovery endpoint returned ${res.status}.`;
|
|
660
|
+
else {
|
|
661
|
+
const json = await res.json();
|
|
662
|
+
if (typeof json.issuer !== "string") discoveryMessage = "Discovery document is missing issuer field.";
|
|
663
|
+
else if (typeof json.authorization_endpoint !== "string") discoveryMessage = "Discovery document is missing authorization_endpoint.";
|
|
664
|
+
else discoveryOk = true;
|
|
665
|
+
}
|
|
666
|
+
} catch (e) {
|
|
667
|
+
discoveryMessage = e instanceof Error ? `Discovery fetch failed: ${e.message}` : "Discovery fetch failed.";
|
|
668
|
+
}
|
|
669
|
+
} else discoveryMessage = "Skipped — issuer or discoveryUrl not set.";
|
|
670
|
+
checks.push({
|
|
671
|
+
name: "discovery_reachable",
|
|
672
|
+
ok: discoveryOk,
|
|
673
|
+
message: discoveryMessage
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
ok: checks.every((c) => c.ok),
|
|
677
|
+
enterpriseId: enterprise._id,
|
|
678
|
+
checks
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
scim: {
|
|
683
|
+
configure: async (ctx, data) => {
|
|
684
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId });
|
|
685
|
+
if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
|
|
686
|
+
const rawToken = generateRandomString(48, INVITE_TOKEN_ALPHABET);
|
|
687
|
+
const tokenHash = await sha256(rawToken);
|
|
688
|
+
const configId = await ctx.runMutation(config.component.public.enterpriseScimConfigUpsert, {
|
|
689
|
+
enterpriseId: enterprise._id,
|
|
690
|
+
groupId: enterprise.groupId,
|
|
691
|
+
status: data.status ?? "active",
|
|
692
|
+
basePath: data.basePath ?? `${requireEnv("CONVEX_SITE_URL")}/api/auth/sso/${enterprise._id}/scim/v2`,
|
|
693
|
+
tokenHash,
|
|
694
|
+
lastRotatedAt: Date.now()
|
|
695
|
+
});
|
|
696
|
+
const auditEventId = await recordEnterpriseAuditEvent(ctx, {
|
|
697
|
+
enterpriseId: enterprise._id,
|
|
698
|
+
groupId: enterprise.groupId,
|
|
699
|
+
eventType: "enterprise.scim.configured",
|
|
700
|
+
actorType: "system",
|
|
701
|
+
subjectType: "enterprise_scim",
|
|
702
|
+
subjectId: configId,
|
|
703
|
+
ok: true
|
|
704
|
+
});
|
|
705
|
+
await emitEnterpriseWebhookDeliveries(ctx, {
|
|
706
|
+
enterpriseId: enterprise._id,
|
|
707
|
+
eventType: "enterprise.scim.configured",
|
|
708
|
+
auditEventId,
|
|
709
|
+
payload: {
|
|
710
|
+
enterpriseId: enterprise._id,
|
|
711
|
+
scimConfigId: configId
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
return {
|
|
715
|
+
ok: true,
|
|
716
|
+
enterpriseId: enterprise._id,
|
|
717
|
+
configId,
|
|
718
|
+
basePath: data.basePath ?? `${requireEnv("CONVEX_SITE_URL")}/api/auth/sso/${enterprise._id}/scim/v2`,
|
|
719
|
+
token: rawToken
|
|
720
|
+
};
|
|
721
|
+
},
|
|
722
|
+
get: async (ctx, enterpriseId) => {
|
|
723
|
+
return await ctx.runQuery(config.component.public.enterpriseScimConfigGetByEnterprise, { enterpriseId });
|
|
724
|
+
},
|
|
725
|
+
getConfigByToken: async (ctx, token) => {
|
|
726
|
+
return await ctx.runQuery(config.component.public.enterpriseScimConfigGetByTokenHash, { tokenHash: await sha256(token) });
|
|
727
|
+
},
|
|
728
|
+
validate: async (ctx, enterpriseId) => {
|
|
729
|
+
const checks = [];
|
|
730
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
731
|
+
if (!enterprise) return {
|
|
732
|
+
ok: false,
|
|
733
|
+
enterpriseId,
|
|
734
|
+
checks: [{
|
|
735
|
+
name: "enterprise_exists",
|
|
736
|
+
ok: false,
|
|
737
|
+
message: "Enterprise not found."
|
|
738
|
+
}]
|
|
739
|
+
};
|
|
740
|
+
const policy = getPolicyFromEnterprise(enterprise);
|
|
741
|
+
const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByEnterprise, { enterpriseId });
|
|
742
|
+
const hasConfig = scimConfig !== null && scimConfig !== void 0;
|
|
743
|
+
checks.push({
|
|
744
|
+
name: "scim_config_exists",
|
|
745
|
+
ok: hasConfig,
|
|
746
|
+
message: hasConfig ? void 0 : "SCIM has not been configured."
|
|
747
|
+
});
|
|
748
|
+
const isActive = hasConfig && scimConfig.status === "active";
|
|
749
|
+
checks.push({
|
|
750
|
+
name: "scim_config_active",
|
|
751
|
+
ok: isActive,
|
|
752
|
+
message: isActive ? void 0 : `SCIM config status is ${hasConfig ? scimConfig.status : "unknown"}.`
|
|
753
|
+
});
|
|
754
|
+
const hasToken = hasConfig && typeof scimConfig.tokenHash === "string" && scimConfig.tokenHash.length > 0;
|
|
755
|
+
checks.push({
|
|
756
|
+
name: "token_hash_set",
|
|
757
|
+
ok: hasToken,
|
|
758
|
+
message: hasToken ? void 0 : "SCIM bearer token has not been set."
|
|
759
|
+
});
|
|
760
|
+
const hasBasePath = hasConfig && typeof scimConfig.basePath === "string" && scimConfig.basePath.length > 0;
|
|
761
|
+
checks.push({
|
|
762
|
+
name: "base_path_set",
|
|
763
|
+
ok: hasBasePath,
|
|
764
|
+
message: hasBasePath ? void 0 : "SCIM basePath is missing."
|
|
765
|
+
});
|
|
766
|
+
return {
|
|
767
|
+
ok: checks.every((c) => c.ok),
|
|
768
|
+
enterpriseId: enterprise._id,
|
|
769
|
+
basePath: hasBasePath ? scimConfig.basePath : null,
|
|
770
|
+
deprovisionMode: policy.provisioning.deprovision.mode,
|
|
771
|
+
checks
|
|
772
|
+
};
|
|
773
|
+
},
|
|
774
|
+
identity: {
|
|
775
|
+
get: async (ctx, data) => {
|
|
776
|
+
return await ctx.runQuery(config.component.public.enterpriseScimIdentityGet, data);
|
|
777
|
+
},
|
|
778
|
+
upsert: async (ctx, data) => {
|
|
779
|
+
return await ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
|
|
780
|
+
...data,
|
|
781
|
+
lastProvisionedAt: Date.now()
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
audit: {
|
|
787
|
+
record: async (ctx, data) => {
|
|
788
|
+
return await recordEnterpriseAuditEvent(ctx, data);
|
|
789
|
+
},
|
|
790
|
+
list: async (ctx, data) => {
|
|
791
|
+
return await ctx.runQuery(config.component.public.enterpriseAuditEventList, data);
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
webhook: {
|
|
795
|
+
endpoint: {
|
|
796
|
+
get: async (ctx, endpointId) => {
|
|
797
|
+
return await ctx.runQuery(config.component.public.enterpriseWebhookEndpointGet, { endpointId });
|
|
798
|
+
},
|
|
799
|
+
create: async (ctx, data) => {
|
|
800
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: data.enterpriseId });
|
|
801
|
+
if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
|
|
802
|
+
const secretHash = await sha256(data.secret);
|
|
803
|
+
const endpointId = await ctx.runMutation(config.component.public.enterpriseWebhookEndpointCreate, {
|
|
804
|
+
enterpriseId: enterprise._id,
|
|
805
|
+
groupId: enterprise.groupId,
|
|
806
|
+
url: data.url,
|
|
807
|
+
secretHash,
|
|
808
|
+
subscriptions: data.subscriptions,
|
|
809
|
+
createdByUserId: data.createdByUserId
|
|
810
|
+
});
|
|
811
|
+
await recordEnterpriseAuditEvent(ctx, {
|
|
812
|
+
enterpriseId: enterprise._id,
|
|
813
|
+
groupId: enterprise.groupId,
|
|
814
|
+
eventType: "enterprise.webhook.endpoint.created",
|
|
815
|
+
actorType: data.createdByUserId ? "user" : "system",
|
|
816
|
+
actorId: data.createdByUserId,
|
|
817
|
+
subjectType: "enterprise_webhook_endpoint",
|
|
818
|
+
subjectId: endpointId,
|
|
819
|
+
ok: true
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
ok: true,
|
|
823
|
+
endpointId
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
list: async (ctx, enterpriseId) => {
|
|
827
|
+
return await ctx.runQuery(config.component.public.enterpriseWebhookEndpointList, { enterpriseId });
|
|
828
|
+
},
|
|
829
|
+
disable: async (ctx, endpointId) => {
|
|
830
|
+
await ctx.runMutation(config.component.public.enterpriseWebhookEndpointUpdate, {
|
|
831
|
+
endpointId,
|
|
832
|
+
data: { status: "disabled" }
|
|
833
|
+
});
|
|
834
|
+
return {
|
|
835
|
+
ok: true,
|
|
836
|
+
endpointId
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
emit: async (ctx, data) => {
|
|
841
|
+
await emitEnterpriseWebhookDeliveries(ctx, data);
|
|
842
|
+
},
|
|
843
|
+
delivery: {
|
|
844
|
+
list: async (ctx, data) => {
|
|
845
|
+
return await ctx.runQuery(config.component.public.enterpriseWebhookDeliveryList, data);
|
|
846
|
+
},
|
|
847
|
+
listReady: async (ctx, limit) => {
|
|
848
|
+
return await ctx.runQuery(config.component.public.enterpriseWebhookDeliveryListReady, {
|
|
849
|
+
now: Date.now(),
|
|
850
|
+
limit
|
|
851
|
+
});
|
|
852
|
+
},
|
|
853
|
+
markDelivered: async (ctx, deliveryId, responseStatus) => {
|
|
854
|
+
await ctx.runMutation(config.component.public.enterpriseWebhookDeliveryPatch, {
|
|
855
|
+
deliveryId,
|
|
856
|
+
data: {
|
|
857
|
+
status: "delivered",
|
|
858
|
+
attemptCount: 1,
|
|
859
|
+
lastAttemptAt: Date.now(),
|
|
860
|
+
lastResponseStatus: responseStatus
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
},
|
|
864
|
+
markFailed: async (ctx, deliveryId, data) => {
|
|
865
|
+
await ctx.runMutation(config.component.public.enterpriseWebhookDeliveryPatch, {
|
|
866
|
+
deliveryId,
|
|
867
|
+
data: {
|
|
868
|
+
status: data.retryAt ? "pending" : "failed",
|
|
869
|
+
attemptCount: data.attemptCount,
|
|
870
|
+
lastAttemptAt: Date.now(),
|
|
871
|
+
lastResponseStatus: data.responseStatus,
|
|
872
|
+
lastError: data.error,
|
|
873
|
+
nextAttemptAt: data.retryAt ?? Date.now()
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
//#endregion
|
|
883
|
+
export { createSsoDomain };
|
|
884
|
+
//# sourceMappingURL=sso.js.map
|