@robelest/convex-auth 0.0.4-preview.28 → 0.0.4-preview.29
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/dist/bin.js +4 -1
- package/dist/component/_generated/component.d.ts +344 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/model.d.ts +25 -25
- package/dist/component/model.js +1 -0
- package/dist/component/public/factors/totp.js +12 -0
- package/dist/component/public/groups/core.js +89 -1
- package/dist/component/public/groups/members.js +39 -1
- package/dist/component/public/identity/accounts.js +22 -3
- package/dist/component/public/identity/users.js +79 -5
- package/dist/component/public/sso/audit.js +5 -2
- package/dist/component/public/sso/core.js +3 -3
- package/dist/component/public/sso/domains.js +7 -3
- package/dist/component/public/sso/scim.js +36 -1
- package/dist/component/public.js +5 -5
- package/dist/component/schema.d.ts +293 -289
- package/dist/component/schema.js +2 -1
- package/dist/core/index.d.ts +63 -27
- package/dist/core/index.js +1 -0
- package/dist/providers/credentials.d.ts +12 -0
- package/dist/providers/password.js +28 -7
- package/dist/server/auth-context.d.ts +17 -0
- package/dist/server/auth-context.js +1 -1
- package/dist/server/auth.d.ts +18 -6
- package/dist/server/auth.js +1 -0
- package/dist/server/context.js +11 -5
- package/dist/server/contract.js +44 -12
- package/dist/server/core.js +146 -149
- package/dist/server/ctxCache.js +94 -0
- package/dist/server/limits.js +39 -9
- package/dist/server/mounts.d.ts +85 -85
- package/dist/server/mutations/credentialsSignIn.js +114 -0
- package/dist/server/mutations/index.js +2 -1
- package/dist/server/mutations/refresh.js +25 -12
- package/dist/server/mutations/retrieve.js +2 -1
- package/dist/server/mutations/signin.js +27 -9
- package/dist/server/mutations/store.js +5 -0
- package/dist/server/mutations/verify.js +32 -8
- package/dist/server/runtime.d.ts +81 -47
- package/dist/server/services/group.js +23 -16
- package/dist/server/sessions.d.ts +21 -0
- package/dist/server/sessions.js +37 -27
- package/dist/server/signin.js +32 -9
- package/dist/server/sso/domain.js +1 -2
- package/dist/server/sso/oidc.js +1 -1
- package/dist/server/tokens.js +19 -3
- package/dist/server/users.js +3 -2
- package/dist/server/utils/span.js +18 -0
- package/package.json +1 -1
- package/README.md +0 -161
|
@@ -17,21 +17,29 @@ function createGroupService(deps) {
|
|
|
17
17
|
kind
|
|
18
18
|
});
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Core OIDC config resolver. Fires the connection fetch (when only the ID
|
|
22
|
+
* is known) in parallel with the secret fetch, and decrypts lazily once the
|
|
23
|
+
* secret is available. All three operations are independent until the final
|
|
24
|
+
* merge, so doing them sequentially was costing 30–60ms per OIDC lookup.
|
|
25
|
+
*/
|
|
26
|
+
const resolveOidcConfigWithSecret = async (ctx, connectionId, preloadedConnection) => {
|
|
27
|
+
const [connection, secret] = await Promise.all([preloadedConnection !== void 0 ? Promise.resolve(preloadedConnection) : getGroupConnection(ctx, config.component.public, connectionId), getGroupConnectionSecret$1(ctx, connectionId, "oidc_client_secret")]);
|
|
28
|
+
if (!connection) throw convexError({
|
|
29
|
+
code: "INVALID_PARAMETERS",
|
|
30
|
+
message: connectionNotFoundError
|
|
31
|
+
});
|
|
21
32
|
const oidc = getOidcConfig(connection.config);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return oidc;
|
|
33
|
+
if (!secret) return oidc;
|
|
34
|
+
const decrypted = await decryptSecret(secret.ciphertext);
|
|
35
|
+
const existingClient = typeof oidc.client === "object" && oidc.client !== null ? oidc.client : {};
|
|
36
|
+
return {
|
|
37
|
+
...oidc,
|
|
38
|
+
client: {
|
|
39
|
+
...existingClient,
|
|
40
|
+
secret: decrypted
|
|
41
|
+
}
|
|
42
|
+
};
|
|
35
43
|
};
|
|
36
44
|
const loadGroupPolicyOrThrow = async (ctx, groupId) => {
|
|
37
45
|
const group = await getGroup(ctx, config.component.public, groupId);
|
|
@@ -84,8 +92,7 @@ function createGroupService(deps) {
|
|
|
84
92
|
};
|
|
85
93
|
};
|
|
86
94
|
const loadConnectionOidcOrThrow = async (ctx, connectionId) => {
|
|
87
|
-
const connection = await loadActiveGroupConnectionOrThrow(ctx, connectionId);
|
|
88
|
-
const oidc = await getGroupConnectionOidcConfigWithSecret(ctx, connection);
|
|
95
|
+
const [connection, oidc] = await Promise.all([loadActiveGroupConnectionOrThrow(ctx, connectionId), resolveOidcConfigWithSecret(ctx, connectionId)]);
|
|
89
96
|
if (oidc.enabled !== true) throw convexError({
|
|
90
97
|
code: "PROVIDER_NOT_CONFIGURED",
|
|
91
98
|
message: "OIDC is not configured for this connection."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "./types.js";
|
|
2
|
+
import { GenericId } from "convex/values";
|
|
3
|
+
|
|
4
|
+
//#region src/server/sessions.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Mutation-side session issuance result. The mutation creates the session
|
|
7
|
+
* and refresh-token rows; JWT signing happens on the action side so the
|
|
8
|
+
* transaction commits without paying the signing CPU cost.
|
|
9
|
+
*/
|
|
10
|
+
type SessionIssuance = {
|
|
11
|
+
userId: GenericId<"User">;
|
|
12
|
+
sessionId: GenericId<"Session">;
|
|
13
|
+
/**
|
|
14
|
+
* Encoded refresh token (`${refreshTokenId}|${sessionId}`), or `null` when
|
|
15
|
+
* the caller opted out of refresh-token issuance (e.g. TOTP step-up).
|
|
16
|
+
*/
|
|
17
|
+
refreshToken: string | null;
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SessionIssuance };
|
|
21
|
+
//# sourceMappingURL=sessions.d.ts.map
|
package/dist/server/sessions.js
CHANGED
|
@@ -3,23 +3,43 @@ import { authDb } from "./db.js";
|
|
|
3
3
|
import { envOptionalNumber, readConfigSync } from "./env.js";
|
|
4
4
|
import { log, maybeRedact } from "./log.js";
|
|
5
5
|
import { REFRESH_TOKEN_DIVIDER, refreshTokenExpirationTime } from "./refresh.js";
|
|
6
|
+
import { withSpan } from "./utils/span.js";
|
|
6
7
|
import { TOKEN_SUB_CLAIM_DIVIDER, generateToken } from "./tokens.js";
|
|
7
8
|
|
|
8
9
|
//#region src/server/sessions.ts
|
|
9
10
|
const DEFAULT_SESSION_TOTAL_DURATION_MS = 1e3 * 60 * 60 * 24 * 30;
|
|
10
11
|
const sessionExpirationTime = (config, now = Date.now()) => now + (config.session?.totalDurationMs ?? readConfigSync(envOptionalNumber("AUTH_SESSION_TOTAL_DURATION_MS")) ?? DEFAULT_SESSION_TOTAL_DURATION_MS);
|
|
11
12
|
const encodeRefreshToken = (refreshTokenId, sessionId) => `${refreshTokenId}${REFRESH_TOKEN_DIVIDER}${sessionId}`;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
13
|
+
/**
|
|
14
|
+
* Convert a {@link SessionIssuance} returned from a mutation into the
|
|
15
|
+
* external `SessionInfo` shape by signing the JWT on the action side.
|
|
16
|
+
*
|
|
17
|
+
* Must be called from an action context because `generateToken` performs
|
|
18
|
+
* RSA-2048 JWT signing that would otherwise block the mutation commit.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
async function finalizeSessionIssuance(config, issuance) {
|
|
23
|
+
return withSpan("convex-auth.session.finalize", { hasRefreshToken: issuance.refreshToken !== null }, async () => {
|
|
24
|
+
if (issuance.refreshToken === null) return {
|
|
25
|
+
userId: issuance.userId,
|
|
26
|
+
sessionId: issuance.sessionId,
|
|
27
|
+
tokens: null
|
|
28
|
+
};
|
|
29
|
+
const token = await generateToken({
|
|
30
|
+
userId: issuance.userId,
|
|
31
|
+
sessionId: issuance.sessionId
|
|
32
|
+
}, config);
|
|
33
|
+
log(LOG_LEVELS.DEBUG, `Generated token ${maybeRedact(token)} and refresh token ${maybeRedact(issuance.refreshToken)} for session ${maybeRedact(issuance.sessionId)}`);
|
|
34
|
+
return {
|
|
35
|
+
userId: issuance.userId,
|
|
36
|
+
sessionId: issuance.sessionId,
|
|
37
|
+
tokens: {
|
|
38
|
+
token,
|
|
39
|
+
refreshToken: issuance.refreshToken
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
23
43
|
}
|
|
24
44
|
/** @internal */
|
|
25
45
|
async function issueSession(ctx, config, args) {
|
|
@@ -30,23 +50,13 @@ async function issueSession(ctx, config, args) {
|
|
|
30
50
|
sessionExpirationTime: sessionExpirationTime(config),
|
|
31
51
|
refreshTokenExpirationTime: args.generateTokens ? refreshTokenExpirationTime(config) : void 0
|
|
32
52
|
});
|
|
33
|
-
|
|
53
|
+
const sessionId = issued.sessionId;
|
|
54
|
+
const refreshTokenId = issued.refreshTokenId;
|
|
55
|
+
return {
|
|
34
56
|
userId: issued.userId,
|
|
35
|
-
sessionId
|
|
36
|
-
|
|
37
|
-
}, args.generateTokens);
|
|
38
|
-
}
|
|
39
|
-
/** @internal */
|
|
40
|
-
async function generateTokensForSession(config, args) {
|
|
41
|
-
const result = {
|
|
42
|
-
token: await generateToken({
|
|
43
|
-
userId: args.userId,
|
|
44
|
-
sessionId: args.sessionId
|
|
45
|
-
}, config),
|
|
46
|
-
refreshToken: encodeRefreshToken(args.refreshTokenId, args.sessionId)
|
|
57
|
+
sessionId,
|
|
58
|
+
refreshToken: args.generateTokens && refreshTokenId !== void 0 ? encodeRefreshToken(refreshTokenId, sessionId) : null
|
|
47
59
|
};
|
|
48
|
-
log(LOG_LEVELS.DEBUG, `Generated token ${maybeRedact(result.token)} and refresh token ${maybeRedact(args.refreshTokenId)} for session ${maybeRedact(args.sessionId)}`);
|
|
49
|
-
return result;
|
|
50
60
|
}
|
|
51
61
|
/** @internal */
|
|
52
62
|
async function deleteSession(ctx, session, config) {
|
|
@@ -68,5 +78,5 @@ async function getAuthSessionId(ctx) {
|
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
//#endregion
|
|
71
|
-
export { deleteSession,
|
|
81
|
+
export { deleteSession, finalizeSessionIssuance, getAuthSessionId, issueSession };
|
|
72
82
|
//# sourceMappingURL=sessions.js.map
|
package/dist/server/signin.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { generateRandomString } from "./random.js";
|
|
2
2
|
import { envOptionalString, readConfigSync, requireEnv } from "./env.js";
|
|
3
3
|
import { log } from "./log.js";
|
|
4
|
-
import { callCreateVerificationCode } from "./mutations/code.js";
|
|
5
4
|
import { withSpan } from "./utils/span.js";
|
|
5
|
+
import { finalizeSessionIssuance } from "./sessions.js";
|
|
6
|
+
import { callCreateVerificationCode } from "./mutations/code.js";
|
|
6
7
|
import { callRefreshSession } from "./mutations/refresh.js";
|
|
7
8
|
import { callSignIn } from "./mutations/signin.js";
|
|
8
9
|
import { callVerifier } from "./mutations/verifier.js";
|
|
@@ -178,7 +179,7 @@ async function handleCredentialsFx(ctx, provider, args, options) {
|
|
|
178
179
|
return withSpan("convex-auth.signin.credentials", {}, async () => {
|
|
179
180
|
let result;
|
|
180
181
|
try {
|
|
181
|
-
result = await provider.authorize(args.params ?? {}, ctx);
|
|
182
|
+
result = await withSpan("convex-auth.signin.credentials.authorize", { providerId: provider.id }, () => provider.authorize(args.params ?? {}, ctx));
|
|
182
183
|
} catch (error) {
|
|
183
184
|
throw asCredentialsError(error);
|
|
184
185
|
}
|
|
@@ -186,19 +187,27 @@ async function handleCredentialsFx(ctx, provider, args, options) {
|
|
|
186
187
|
kind: "signedIn",
|
|
187
188
|
signedIn: null
|
|
188
189
|
};
|
|
190
|
+
const hintedHasTotp = result.hasTotp;
|
|
191
|
+
const preIssuedIssuance = result.issuance;
|
|
189
192
|
let hasTotpEnrolled;
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
if (hintedHasTotp === false) hasTotpEnrolled = false;
|
|
194
|
+
else if (hintedHasTotp === true) hasTotpEnrolled = true;
|
|
195
|
+
else if (preIssuedIssuance !== void 0 && preIssuedIssuance.refreshToken === null) hasTotpEnrolled = true;
|
|
196
|
+
else try {
|
|
197
|
+
hasTotpEnrolled = await withSpan("convex-auth.signin.credentials.totp-check", { hinted: hintedHasTotp ?? "unknown" }, () => queryTotpVerifiedByUserId(ctx, result.userId)) !== null;
|
|
192
198
|
} catch (error) {
|
|
193
199
|
throw asConvexError(error, "INTERNAL_ERROR", "Failed to load TOTP enrollment.");
|
|
194
200
|
}
|
|
195
201
|
if (hasTotpEnrolled) {
|
|
196
|
-
try {
|
|
197
|
-
await
|
|
202
|
+
if (preIssuedIssuance === void 0) try {
|
|
203
|
+
await withSpan("convex-auth.signin.credentials.issue-session", {
|
|
204
|
+
generateTokens: false,
|
|
205
|
+
totpStepUp: true
|
|
206
|
+
}, () => callSignIn(ctx, {
|
|
198
207
|
userId: result.userId,
|
|
199
208
|
sessionId: result.sessionId,
|
|
200
209
|
generateTokens: false
|
|
201
|
-
});
|
|
210
|
+
}));
|
|
202
211
|
} catch (error) {
|
|
203
212
|
throw asConvexError(error, "INTERNAL_ERROR", "Failed to start TOTP sign-in.");
|
|
204
213
|
}
|
|
@@ -213,13 +222,27 @@ async function handleCredentialsFx(ctx, provider, args, options) {
|
|
|
213
222
|
verifier
|
|
214
223
|
};
|
|
215
224
|
}
|
|
225
|
+
if (preIssuedIssuance !== void 0) try {
|
|
226
|
+
return {
|
|
227
|
+
kind: "signedIn",
|
|
228
|
+
signedIn: await withSpan("convex-auth.signin.credentials.finalize", {
|
|
229
|
+
generateTokens: options.generateTokens,
|
|
230
|
+
fromAuthorize: true
|
|
231
|
+
}, () => finalizeSessionIssuance(ctx.auth.config, {
|
|
232
|
+
...preIssuedIssuance,
|
|
233
|
+
refreshToken: options.generateTokens ? preIssuedIssuance.refreshToken : null
|
|
234
|
+
}))
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw asConvexError(error, "INTERNAL_ERROR", "Failed to finalize sign-in.");
|
|
238
|
+
}
|
|
216
239
|
let idsAndTokens;
|
|
217
240
|
try {
|
|
218
|
-
idsAndTokens = await callSignIn(ctx, {
|
|
241
|
+
idsAndTokens = await withSpan("convex-auth.signin.credentials.issue-session", { generateTokens: options.generateTokens }, () => callSignIn(ctx, {
|
|
219
242
|
userId: result.userId,
|
|
220
243
|
sessionId: result.sessionId,
|
|
221
244
|
generateTokens: options.generateTokens
|
|
222
|
-
});
|
|
245
|
+
}));
|
|
223
246
|
} catch (error) {
|
|
224
247
|
throw asConvexError(error, "INTERNAL_ERROR", "Failed to complete sign-in.");
|
|
225
248
|
}
|
|
@@ -184,12 +184,11 @@ function createGroupConnectionDomain(deps) {
|
|
|
184
184
|
};
|
|
185
185
|
},
|
|
186
186
|
status: async (ctx, connectionId) => {
|
|
187
|
-
const connection = await getGroupConnection(ctx, config.component.public, connectionId);
|
|
187
|
+
const [connection, domains] = await Promise.all([getGroupConnection(ctx, config.component.public, connectionId), listConnectionDomains(ctx, config.component.public, connectionId)]);
|
|
188
188
|
if (connection === null) throw convexError({
|
|
189
189
|
code: "INVALID_PARAMETERS",
|
|
190
190
|
message: connectionNotFoundError
|
|
191
191
|
});
|
|
192
|
-
const domains = await listConnectionDomains(ctx, config.component.public, connectionId);
|
|
193
192
|
const primaryDomain = domains.find((domain) => domain.isPrimary) ?? null;
|
|
194
193
|
const verifiedDomains = domains.filter((domain) => domain.verifiedAt !== void 0);
|
|
195
194
|
const pendingChallenges = (await Promise.all(domains.map(async (domain) => {
|
package/dist/server/sso/oidc.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { log } from "../log.js";
|
|
2
|
+
import { withSpan } from "../utils/span.js";
|
|
2
3
|
import { createCache } from "../utils/cache.js";
|
|
3
4
|
import { retryWithBackoff } from "../utils/retry.js";
|
|
4
|
-
import { withSpan } from "../utils/span.js";
|
|
5
5
|
import { finalizeNormalizedProfile, normalizeStringArray } from "./profile.js";
|
|
6
6
|
import { getGroupOidcUrls, groupOidcProviderId } from "./shared.js";
|
|
7
7
|
import { sha256 } from "@oslojs/crypto/sha2";
|
package/dist/server/tokens.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { generateRandomString } from "./random.js";
|
|
2
2
|
import { requireEnv } from "./env.js";
|
|
3
|
+
import { withSpan } from "./utils/span.js";
|
|
3
4
|
import { SignJWT, importPKCS8 } from "jose";
|
|
4
5
|
|
|
5
6
|
//#region src/server/tokens.ts
|
|
@@ -9,19 +10,34 @@ const TOKEN_JTI_LENGTH = 24;
|
|
|
9
10
|
const TOKEN_JTI_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
10
11
|
let cachedPrivateKeyPromise = null;
|
|
11
12
|
let cachedIssuer = null;
|
|
13
|
+
const JWT_ALG = "EdDSA";
|
|
12
14
|
const getPrivateKey = () => {
|
|
13
|
-
if (cachedPrivateKeyPromise === null)
|
|
15
|
+
if (cachedPrivateKeyPromise === null) try {
|
|
16
|
+
cachedPrivateKeyPromise = importPKCS8(requireEnv("JWT_PRIVATE_KEY"), JWT_ALG).catch((error) => {
|
|
17
|
+
cachedPrivateKeyPromise = null;
|
|
18
|
+
throw error;
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
cachedPrivateKeyPromise = null;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
14
24
|
return cachedPrivateKeyPromise;
|
|
15
25
|
};
|
|
16
26
|
const getIssuer = () => {
|
|
17
27
|
if (cachedIssuer === null) cachedIssuer = requireEnv("CONVEX_SITE_URL");
|
|
18
28
|
return cachedIssuer;
|
|
19
29
|
};
|
|
30
|
+
try {
|
|
31
|
+
getPrivateKey().catch(() => {});
|
|
32
|
+
} catch {}
|
|
33
|
+
try {
|
|
34
|
+
getIssuer();
|
|
35
|
+
} catch {}
|
|
20
36
|
/** @internal */
|
|
21
37
|
async function generateToken(args, config) {
|
|
22
|
-
const privateKey = await getPrivateKey();
|
|
38
|
+
const privateKey = await withSpan("convex-auth.tokens.import-key", { alg: JWT_ALG }, () => getPrivateKey());
|
|
23
39
|
const expirationTime = new Date(Date.now() + (config.jwt?.durationMs ?? DEFAULT_JWT_DURATION_MS));
|
|
24
|
-
return await new SignJWT({ sub: args.userId + TOKEN_SUB_CLAIM_DIVIDER + args.sessionId }).setProtectedHeader({ alg:
|
|
40
|
+
return await withSpan("convex-auth.tokens.sign", { alg: JWT_ALG }, () => new SignJWT({ sub: args.userId + TOKEN_SUB_CLAIM_DIVIDER + args.sessionId }).setProtectedHeader({ alg: JWT_ALG }).setIssuedAt().setJti(generateRandomString(TOKEN_JTI_LENGTH, TOKEN_JTI_ALPHABET)).setIssuer(getIssuer()).setAudience("convex").setExpirationTime(expirationTime).sign(privateKey));
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
//#endregion
|
package/dist/server/users.js
CHANGED
|
@@ -61,8 +61,9 @@ async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount
|
|
|
61
61
|
const shouldLinkViaPhone = args.shouldLinkViaPhone || phoneVerified || provider.type === "phone";
|
|
62
62
|
let userId = existingUserId ?? existingUserIdOverride;
|
|
63
63
|
if (existingUserId === null) {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
64
|
+
const [emailLookup, phoneLookup] = await Promise.all([typeof profile.email === "string" && shouldLinkViaEmail ? uniqueUserWithVerifiedEmail(ctx, profile.email, config) : Promise.resolve(null), typeof profile.phone === "string" && shouldLinkViaPhone ? uniqueUserWithVerifiedPhone(ctx, profile.phone, config) : Promise.resolve(null)]);
|
|
65
|
+
const existingUserWithVerifiedEmailId = emailLookup?._id ?? null;
|
|
66
|
+
const existingUserWithVerifiedPhoneId = phoneLookup?._id ?? null;
|
|
66
67
|
if (existingUserWithVerifiedEmailId !== null && existingUserWithVerifiedPhoneId !== null) {
|
|
67
68
|
log(LOG_LEVELS.DEBUG, `Found existing email and phone verified users, so not linking: email: ${existingUserWithVerifiedEmailId}, phone: ${existingUserWithVerifiedPhoneId}`);
|
|
68
69
|
userId = null;
|
|
@@ -7,13 +7,31 @@ import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
const tracer = trace.getTracer("convex-auth");
|
|
10
|
+
const INVALID_TRACE_ID = "00000000000000000000000000000000";
|
|
11
|
+
const ACTIVE_CHECK_INTERVAL_MS = 5e3;
|
|
12
|
+
let cachedActiveAt = 0;
|
|
13
|
+
let cachedActive = false;
|
|
14
|
+
function isTracingActive() {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
if (now - cachedActiveAt < ACTIVE_CHECK_INTERVAL_MS) return cachedActive;
|
|
17
|
+
const probe = tracer.startSpan("__convex-auth.probe");
|
|
18
|
+
const active = probe.isRecording() || probe.spanContext().traceId !== INVALID_TRACE_ID;
|
|
19
|
+
probe.end();
|
|
20
|
+
cachedActive = active;
|
|
21
|
+
cachedActiveAt = now;
|
|
22
|
+
return active;
|
|
23
|
+
}
|
|
10
24
|
/**
|
|
11
25
|
* Run `fn` inside an OpenTelemetry span.
|
|
12
26
|
*
|
|
13
27
|
* If `fn` throws, the span is marked as errored and the exception is
|
|
14
28
|
* recorded before re-throwing.
|
|
29
|
+
*
|
|
30
|
+
* When no tracer provider is registered, this is a cheap passthrough that
|
|
31
|
+
* skips span creation altogether.
|
|
15
32
|
*/
|
|
16
33
|
async function withSpan(name, attributes, fn) {
|
|
34
|
+
if (!isTracingActive()) return fn();
|
|
17
35
|
return tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
18
36
|
try {
|
|
19
37
|
return await fn();
|
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# @robelest/convex-auth
|
|
2
|
-
|
|
3
|
-
Component-first authentication for [Convex](https://convex.dev). One component,
|
|
4
|
-
one setup API, full TypeScript support.
|
|
5
|
-
|
|
6
|
-
## Features
|
|
7
|
-
|
|
8
|
-
- **Convex-native setup API** — `createAuth(components.auth, { providers })`
|
|
9
|
-
gives you everything.
|
|
10
|
-
- **First-party OAuth providers** — Google, GitHub, Apple, and Microsoft with
|
|
11
|
-
product-owned wrappers and sane defaults.
|
|
12
|
-
- **Fluent Convex builders (recommended)** — cleaner auth-aware API handling
|
|
13
|
-
with middleware and explicit `.public()` / `.internal()` exports.
|
|
14
|
-
- **Password, passkeys, TOTP, magic links, OTP, phone, anonymous** — all built
|
|
15
|
-
in.
|
|
16
|
-
- **Device Authorization (RFC 8628)** — authenticate CLIs, smart TVs, and IoT
|
|
17
|
-
devices.
|
|
18
|
-
- **API keys** — scoped permissions, SHA-256 hashed storage, optional rate
|
|
19
|
-
limiting.
|
|
20
|
-
- **Groups, memberships, invites** — hierarchical multi-tenancy with roles.
|
|
21
|
-
- **SSR support** — framework-agnostic httpOnly cookie API (SvelteKit, TanStack
|
|
22
|
-
Start, Next.js).
|
|
23
|
-
- **Context enrichment** — zero-boilerplate `ctx.auth.userId` via `auth.ctx()`.
|
|
24
|
-
|
|
25
|
-
## Install
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
bun add @robelest/convex-auth
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
> Renamed package: if you are migrating from earlier previews, replace
|
|
32
|
-
> `@convex-dev/auth` with `@robelest/convex-auth` in imports and CLI commands.
|
|
33
|
-
|
|
34
|
-
## Quick Start
|
|
35
|
-
|
|
36
|
-
Before running setup:
|
|
37
|
-
|
|
38
|
-
- Run from your app project root (must include `package.json`).
|
|
39
|
-
- Make sure a Convex deployment can be resolved. By default the CLI reads a
|
|
40
|
-
typed `CONVEX_DEPLOYMENT` like `dev:my-deployment` (usually set by
|
|
41
|
-
`npx convex dev`), or you can pass `--prod`, `--preview-name`,
|
|
42
|
-
`--deployment-name`, `--url`, or `--admin-key`.
|
|
43
|
-
- Use `--url` for explicit or self-hosted targets. Typed deployment and
|
|
44
|
-
admin-key parsing only applies to Convex Cloud selections.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
bunx @robelest/convex-auth
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The interactive CLI sets up your Convex component, auth config, and HTTP routes
|
|
51
|
-
in under a minute.
|
|
52
|
-
|
|
53
|
-
## Manual Setup
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
// convex/convex.config.ts
|
|
57
|
-
import { defineApp } from "convex/server";
|
|
58
|
-
import auth from "@robelest/convex-auth/convex.config";
|
|
59
|
-
|
|
60
|
-
const app = defineApp();
|
|
61
|
-
app.use(auth);
|
|
62
|
-
export default app;
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
// convex/auth.ts
|
|
67
|
-
import { createAuth } from "@robelest/convex-auth/component";
|
|
68
|
-
import { github } from "@robelest/convex-auth/providers";
|
|
69
|
-
import { components } from "./_generated/api";
|
|
70
|
-
|
|
71
|
-
const auth = createAuth(components.auth, {
|
|
72
|
-
providers: [
|
|
73
|
-
github({
|
|
74
|
-
clientId: process.env.AUTH_GITHUB_ID!,
|
|
75
|
-
clientSecret: process.env.AUTH_GITHUB_SECRET!,
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
export { auth };
|
|
81
|
-
export const { signIn, signOut, store } = auth;
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
// convex/http.ts
|
|
86
|
-
import { httpRouter } from "convex/server";
|
|
87
|
-
import { auth } from "./auth";
|
|
88
|
-
|
|
89
|
-
const http = httpRouter();
|
|
90
|
-
auth.http.add(http);
|
|
91
|
-
export default http;
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Recommended Convex API Handling (`fluent-convex`)
|
|
95
|
-
|
|
96
|
-
For new projects, we recommend `fluent-convex` for auth middleware composition
|
|
97
|
-
and cleaner API exports.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
// convex/functions.ts
|
|
101
|
-
import { createBuilder } from "fluent-convex";
|
|
102
|
-
import { WithZod } from "fluent-convex/zod";
|
|
103
|
-
import type { DataModel } from "./_generated/dataModel";
|
|
104
|
-
import { auth } from "./auth";
|
|
105
|
-
|
|
106
|
-
const convex = createBuilder<DataModel>();
|
|
107
|
-
|
|
108
|
-
// `auth.context(ctx)` resolves { userId, user, groupId, role, grants }
|
|
109
|
-
// and throws before the handler runs when unauthenticated.
|
|
110
|
-
const withRequiredAuth = convex.createMiddleware(async (ctx, next) => {
|
|
111
|
-
return next({ ...ctx, auth: await auth.context(ctx) });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
export const query = convex.query().use(withRequiredAuth).extend(WithZod);
|
|
115
|
-
export const mutation = convex.mutation().use(withRequiredAuth).extend(WithZod);
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`auth.ctx()` works with `convex-helpers` and other custom builder setups.
|
|
119
|
-
|
|
120
|
-
## Providers
|
|
121
|
-
|
|
122
|
-
| Provider | Import |
|
|
123
|
-
| ----------------- | ------------------------------------------------------------- |
|
|
124
|
-
| Google | `import { google } from "@robelest/convex-auth/providers"` |
|
|
125
|
-
| GitHub | `import { github } from "@robelest/convex-auth/providers"` |
|
|
126
|
-
| Apple | `import { apple } from "@robelest/convex-auth/providers"` |
|
|
127
|
-
| Microsoft | `import { microsoft } from "@robelest/convex-auth/providers"` |
|
|
128
|
-
| Password | `import { password } from "@robelest/convex-auth/providers"` |
|
|
129
|
-
| Passkey | `import { passkey } from "@robelest/convex-auth/providers"` |
|
|
130
|
-
| TOTP | `import { totp } from "@robelest/convex-auth/providers"` |
|
|
131
|
-
| Phone/SMS | `import { phone } from "@robelest/convex-auth/providers"` |
|
|
132
|
-
| Anonymous | `import { anonymous } from "@robelest/convex-auth/providers"` |
|
|
133
|
-
| Device (RFC 8628) | `import { device } from "@robelest/convex-auth/providers"` |
|
|
134
|
-
| Custom OAuth | `import { custom } from "@robelest/convex-auth/providers"` |
|
|
135
|
-
|
|
136
|
-
OAuth provider-specific setup details live in the docs:
|
|
137
|
-
|
|
138
|
-
- Google, GitHub, Apple, Microsoft: `/getting-started/providers`
|
|
139
|
-
- Required env vars: `/getting-started/environment`
|
|
140
|
-
|
|
141
|
-
## Group SSO
|
|
142
|
-
|
|
143
|
-
The group direction is a headless SDK/API rather than a hosted admin UI.
|
|
144
|
-
|
|
145
|
-
- `auth.group.sso.*` stores group connection config alongside `auth.group`, with
|
|
146
|
-
group-level policy owned by `auth.group.sso.policy.*`.
|
|
147
|
-
- Standardized helpers are exposed as `auth.group.sso.connection.*`,
|
|
148
|
-
`auth.group.sso.connection.domain.*`, `auth.group.sso.oidc.*`,
|
|
149
|
-
`auth.group.sso.saml.*`, `auth.group.sso.policy.*`,
|
|
150
|
-
`auth.group.sso.audit.list`, `auth.group.sso.webhook.endpoint.*`,
|
|
151
|
-
`auth.group.sso.signIn`, `auth.group.sso.metadata`, and
|
|
152
|
-
`auth.group.sso.scim.configure/get/validate`.
|
|
153
|
-
- Group SSO helpers are server-side primitives. Consumers can build and expose
|
|
154
|
-
their own Convex RPC wrappers when needed.
|
|
155
|
-
- `auth.group.sso.metadata(...)` uses the local `@robelest/samlify` package to
|
|
156
|
-
parse IdP metadata and generate SP metadata from the same setup state.
|
|
157
|
-
|
|
158
|
-
## Documentation
|
|
159
|
-
|
|
160
|
-
See the full [README](https://github.com/robelest/convex-auth#readme) for
|
|
161
|
-
detailed usage, API reference, SSR integration, and more.
|