@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28
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 +3 -5
- package/dist/bin.js +6488 -1571
- package/dist/browser/index.js +10 -7
- package/dist/browser/locks.js +3 -5
- package/dist/browser/navigation.js +7 -10
- package/dist/browser/runtime.js +35 -33
- package/dist/client/core/types.js +17 -0
- package/dist/client/factors/device.js +26 -19
- package/dist/client/index.js +151 -163
- package/dist/client/runtime/proxy.js +6 -6
- package/dist/client/services/adapters.js +3 -7
- package/dist/client/services/http.js +2 -5
- package/dist/client/services/resolve.js +5 -11
- package/dist/client/services/runtime.js +2 -5
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/index.d.ts +3 -3
- package/dist/component/model.d.ts +25 -25
- package/dist/component/public/identity/sessions.js +38 -1
- package/dist/component/public/identity/tokens.js +81 -3
- package/dist/component/public/identity/verifiers.js +9 -3
- package/dist/component/public.js +3 -3
- package/dist/component/schema.d.ts +320 -320
- package/dist/core/index.d.ts +380 -0
- package/dist/core/index.js +83 -0
- package/dist/otel.d.ts +13 -17
- package/dist/otel.js +39 -49
- package/dist/providers/email.d.ts +2 -2
- package/dist/providers/password.js +8 -16
- package/dist/providers/phone.js +2 -9
- package/dist/server/auth-context.d.ts +204 -0
- package/dist/server/auth-context.js +76 -0
- package/dist/server/auth.d.ts +25 -187
- package/dist/server/auth.js +5 -96
- package/dist/server/componentContext.d.ts +12 -0
- package/dist/server/componentContext.js +1 -0
- package/dist/server/config.js +1 -12
- package/dist/server/constants.js +6 -0
- package/dist/server/contract.d.ts +1 -1
- package/dist/server/core.js +5 -14
- package/dist/server/crypto.js +26 -18
- package/dist/server/db.js +6 -1
- package/dist/server/device.js +88 -78
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.js +74 -86
- package/dist/server/index.d.ts +2 -1
- package/dist/server/limits.js +22 -15
- package/dist/server/mounts.d.ts +103 -103
- package/dist/server/mutations/account.js +6 -4
- package/dist/server/mutations/invalidate.js +3 -6
- package/dist/server/mutations/oauth.js +86 -88
- package/dist/server/mutations/refresh.js +45 -87
- package/dist/server/mutations/register.js +19 -19
- package/dist/server/mutations/retrieve.js +17 -15
- package/dist/server/mutations/signature.js +9 -13
- package/dist/server/mutations/signin.js +7 -3
- package/dist/server/mutations/signout.js +10 -15
- package/dist/server/mutations/store.js +22 -12
- package/dist/server/mutations/verifier.js +11 -6
- package/dist/server/mutations/verify.js +55 -46
- package/dist/server/oauth/runtime.js +27 -25
- package/dist/server/passkey.js +299 -250
- package/dist/server/prefetch.js +283 -281
- package/dist/server/refresh.js +7 -60
- package/dist/server/runtime.d.ts +82 -206
- package/dist/server/runtime.js +63 -56
- package/dist/server/services/config.js +5 -3
- package/dist/server/services/logger.js +2 -4
- package/dist/server/services/providers.js +2 -4
- package/dist/server/services/refresh.js +2 -4
- package/dist/server/services/resolve.js +15 -14
- package/dist/server/services/signin.js +2 -4
- package/dist/server/sessions.js +32 -33
- package/dist/server/signin.js +177 -142
- package/dist/server/sso/domain.d.ts +20 -68
- package/dist/server/sso/domain.js +444 -413
- package/dist/server/sso/http.js +53 -59
- package/dist/server/sso/oidc.js +94 -80
- package/dist/server/tokens.js +13 -3
- package/dist/server/totp.js +153 -116
- package/dist/server/types.d.ts +2 -2
- package/dist/server/users.js +18 -23
- package/dist/server/utils/cache.js +51 -0
- package/dist/server/utils/dispatch.js +36 -0
- package/dist/server/utils/retry.js +24 -0
- package/dist/server/utils/span.js +32 -0
- package/dist/shared/errors.js +9 -3
- package/dist/shared/log.js +20 -22
- package/package.json +41 -33
package/dist/server/totp.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { userIdFromIdentitySubject } from "./identity.js";
|
|
2
|
-
import { authFlowError } from "../shared/errors.js";
|
|
3
|
-
import { toConvexError } from "./errors.js";
|
|
4
|
-
import { callVerifierSignature } from "./mutations/signature.js";
|
|
5
2
|
import { callSignIn } from "./mutations/signin.js";
|
|
6
3
|
import { callVerifier } from "./mutations/verifier.js";
|
|
4
|
+
import { authFlowError } from "../shared/errors.js";
|
|
5
|
+
import { toConvexError } from "./errors.js";
|
|
7
6
|
import { mutateTotpInsert, mutateTotpMarkVerified, mutateTotpUpdateLastUsed, mutateVerifierDelete, queryTotpById, queryTotpVerifiedByUserId, queryUserById, queryVerifierById } from "./types.js";
|
|
8
7
|
import { ConvexError } from "convex/values";
|
|
9
|
-
import { Effect, Match } from "effect";
|
|
10
8
|
import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding";
|
|
11
9
|
import { createTOTPKeyURI, verifyTOTPWithGracePeriod } from "@oslojs/otp";
|
|
12
10
|
|
|
@@ -26,135 +24,174 @@ const TOTP_FLOWS = [
|
|
|
26
24
|
];
|
|
27
25
|
const convexError = (code, message) => toConvexError(authFlowError(code, message));
|
|
28
26
|
const asConvexError = (error, code, message) => error instanceof ConvexError ? error : error instanceof Error ? toConvexError(authFlowError(code, error.message || message)) : convexError(code, message);
|
|
29
|
-
|
|
27
|
+
function resolveTotpFlow(params) {
|
|
30
28
|
const flow = params.flow;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
if (typeof flow === "string" && TOTP_FLOWS.includes(flow)) return flow;
|
|
30
|
+
throw convexError("TOTP_MISSING_FLOW", "Missing `flow` parameter. Expected one of: setup, confirm, verify");
|
|
31
|
+
}
|
|
32
|
+
function requireTotpVerifier(verifier) {
|
|
33
|
+
if (verifier != null) return verifier;
|
|
34
|
+
throw convexError("TOTP_MISSING_VERIFIER", "Missing verifier for TOTP operation.");
|
|
35
|
+
}
|
|
36
|
+
function requireTotpCode(params) {
|
|
37
|
+
if (typeof params.code === "string") return params.code;
|
|
38
|
+
throw convexError("TOTP_MISSING_CODE", "Missing TOTP code.");
|
|
39
|
+
}
|
|
40
|
+
function requireTotpId(params) {
|
|
41
|
+
if (typeof params.totpId === "string") return params.totpId;
|
|
42
|
+
throw convexError("TOTP_MISSING_ID", "Missing TOTP enrollment ID.");
|
|
43
|
+
}
|
|
44
|
+
function resolveTotpDispatch(params, verifier) {
|
|
45
|
+
const flow = resolveTotpFlow(params);
|
|
46
|
+
if (flow === "setup") return {
|
|
47
|
+
flow: "setup",
|
|
48
|
+
params
|
|
46
49
|
};
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
if (flow === "confirm") {
|
|
51
|
+
const resolvedVerifier$1 = requireTotpVerifier(verifier);
|
|
52
|
+
return {
|
|
53
|
+
flow: "confirm",
|
|
54
|
+
code: requireTotpCode(params),
|
|
55
|
+
totpId: requireTotpId(params),
|
|
56
|
+
verifier: resolvedVerifier$1
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const resolvedVerifier = requireTotpVerifier(verifier);
|
|
49
60
|
return {
|
|
50
61
|
flow: "verify",
|
|
51
|
-
code:
|
|
62
|
+
code: requireTotpCode(params),
|
|
52
63
|
verifier: resolvedVerifier
|
|
53
64
|
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
}
|
|
66
|
+
async function requireAuthenticatedUserId(ctx) {
|
|
67
|
+
let identity;
|
|
68
|
+
try {
|
|
69
|
+
identity = await ctx.auth.getUserIdentity();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw asConvexError(error, "INTERNAL_ERROR", String(error));
|
|
72
|
+
}
|
|
73
|
+
if (identity === null) throw convexError("TOTP_AUTH_REQUIRED", "Sign in first, then set up two-factor authentication.");
|
|
74
|
+
return userIdFromIdentitySubject(identity.subject);
|
|
75
|
+
}
|
|
59
76
|
/** @internal */
|
|
60
|
-
const handleTotp = (ctx, provider, args) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
const handleTotp = async (ctx, provider, args) => {
|
|
78
|
+
const dispatch = resolveTotpDispatch(args.params ?? {}, args.verifier);
|
|
79
|
+
const handler = {
|
|
80
|
+
setup: async () => {
|
|
81
|
+
const { params: setupParams } = dispatch;
|
|
82
|
+
const userId = await requireAuthenticatedUserId(ctx);
|
|
83
|
+
const secret = new Uint8Array(20);
|
|
84
|
+
crypto.getRandomValues(secret);
|
|
85
|
+
let accountName = setupParams.accountName;
|
|
86
|
+
if (!accountName) {
|
|
87
|
+
let user;
|
|
88
|
+
try {
|
|
89
|
+
user = await queryUserById(ctx, userId);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
accountName = user?.email ?? "user";
|
|
94
|
+
}
|
|
95
|
+
const uri = createTOTPKeyURI(provider.options.issuer, accountName, secret, provider.options.period, provider.options.digits);
|
|
96
|
+
const base32Secret = encodeBase32LowerCaseNoPadding(secret);
|
|
97
|
+
let verifier;
|
|
98
|
+
try {
|
|
99
|
+
verifier = await callVerifier(ctx, JSON.stringify({
|
|
81
100
|
secret: Array.from(secret),
|
|
82
101
|
userId,
|
|
83
102
|
digits: provider.options.digits,
|
|
84
103
|
period: provider.options.period
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
secret: base32Secret,
|
|
93
|
-
verifier,
|
|
94
|
-
totpId: yield* Effect.tryPromise({
|
|
95
|
-
try: () => mutateTotpInsert(ctx, {
|
|
104
|
+
}));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
let totpId;
|
|
109
|
+
try {
|
|
110
|
+
totpId = await mutateTotpInsert(ctx, {
|
|
96
111
|
userId,
|
|
97
112
|
secret: secret.buffer.slice(secret.byteOffset, secret.byteOffset + secret.byteLength),
|
|
98
113
|
digits: provider.options.digits,
|
|
99
114
|
period: provider.options.period,
|
|
100
115
|
verified: false,
|
|
101
|
-
name: typeof
|
|
116
|
+
name: typeof setupParams.name === "string" ? setupParams.name : void 0,
|
|
102
117
|
createdAt: Date.now()
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
kind: "totpSetup",
|
|
124
|
+
uri,
|
|
125
|
+
secret: base32Secret,
|
|
126
|
+
verifier,
|
|
127
|
+
totpId
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
confirm: async () => {
|
|
131
|
+
const { code, totpId, verifier } = dispatch;
|
|
132
|
+
const userId = await requireAuthenticatedUserId(ctx);
|
|
133
|
+
let doc;
|
|
134
|
+
try {
|
|
135
|
+
doc = await queryTotpById(ctx, totpId);
|
|
136
|
+
} catch {
|
|
137
|
+
throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
138
|
+
}
|
|
139
|
+
if (doc === null) throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
140
|
+
if (doc.verified) throw convexError("TOTP_ALREADY_VERIFIED", "TOTP enrollment is already verified.");
|
|
141
|
+
if (!verifyTOTPWithGracePeriod(new Uint8Array(doc.secret), provider.options.period, provider.options.digits, code, 30)) throw convexError("TOTP_INVALID_CODE", "Invalid TOTP code.");
|
|
142
|
+
let signInResult;
|
|
143
|
+
try {
|
|
144
|
+
await mutateTotpMarkVerified(ctx, totpId, Date.now());
|
|
145
|
+
await mutateVerifierDelete(ctx, verifier);
|
|
146
|
+
signInResult = await callSignIn(ctx, {
|
|
147
|
+
userId,
|
|
148
|
+
generateTokens: true
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw asConvexError(error, "INTERNAL_ERROR", String(error));
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
kind: "signedIn",
|
|
155
|
+
signedIn: signInResult
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
verify: async () => {
|
|
159
|
+
const { code, verifier } = dispatch;
|
|
160
|
+
let doc;
|
|
161
|
+
try {
|
|
162
|
+
doc = await queryVerifierById(ctx, verifier);
|
|
163
|
+
} catch {
|
|
164
|
+
throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
165
|
+
}
|
|
166
|
+
if (doc === null) throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
167
|
+
const userId = JSON.parse(doc.signature).userId;
|
|
168
|
+
let totp;
|
|
169
|
+
try {
|
|
170
|
+
totp = await queryTotpVerifiedByUserId(ctx, userId);
|
|
171
|
+
} catch {
|
|
172
|
+
throw convexError("TOTP_NO_ENROLLMENT", "No verified TOTP enrollment found.");
|
|
173
|
+
}
|
|
174
|
+
if (totp === null) throw convexError("TOTP_NO_ENROLLMENT", "No verified TOTP enrollment found.");
|
|
175
|
+
if (!verifyTOTPWithGracePeriod(new Uint8Array(totp.secret), totp.period, totp.digits, code, 30)) throw convexError("TOTP_INVALID_CODE", "Invalid TOTP code.");
|
|
176
|
+
let signInResult;
|
|
177
|
+
try {
|
|
178
|
+
await mutateTotpUpdateLastUsed(ctx, totp._id, Date.now());
|
|
179
|
+
await mutateVerifierDelete(ctx, verifier);
|
|
180
|
+
signInResult = await callSignIn(ctx, {
|
|
181
|
+
userId,
|
|
182
|
+
generateTokens: true
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw asConvexError(error, "INTERNAL_ERROR", String(error));
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
kind: "signedIn",
|
|
189
|
+
signedIn: signInResult
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}[dispatch.flow];
|
|
193
|
+
if (!handler) throw convexError("TOTP_MISSING_FLOW", `Unknown TOTP flow: ${dispatch.flow}`);
|
|
194
|
+
return handler();
|
|
158
195
|
};
|
|
159
196
|
|
|
160
197
|
//#endregion
|
package/dist/server/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { vApiKeyDoc } from "../component/model.js";
|
|
|
2
2
|
import { _default } from "../component/schema.js";
|
|
3
3
|
import { CredentialsConfig } from "../providers/credentials.js";
|
|
4
4
|
import { GenericId, Infer, Value } from "convex/values";
|
|
5
|
-
import { AnyDataModel, DataModelFromSchemaDefinition, DocumentByName, FunctionReference, GenericActionCtx, GenericDataModel, GenericMutationCtx, TableNamesInDataModel } from "convex/server";
|
|
5
|
+
import { AnyDataModel, DataModelFromSchemaDefinition, DocumentByName, FunctionReference, GenericActionCtx, GenericDataModel, GenericMutationCtx, GenericQueryCtx, TableNamesInDataModel } from "convex/server";
|
|
6
6
|
|
|
7
7
|
//#region src/server/types.d.ts
|
|
8
8
|
/**
|
|
@@ -1157,5 +1157,5 @@ type AuthDataModel = DataModelFromSchemaDefinition<typeof _default>;
|
|
|
1157
1157
|
type Doc<T extends TableNamesInDataModel<AuthDataModel>> = GenericDoc<AuthDataModel, T>;
|
|
1158
1158
|
type KeyDoc = Infer<typeof vApiKeyDoc>;
|
|
1159
1159
|
//#endregion
|
|
1160
|
-
export { AuthAuthorizationConfig, AuthGrant, AuthProviderConfig, AuthRoleId, ConvexAuthConfig, ConvexAuthMaterializedConfig, ConvexCredentialsConfig, CorsConfig, DeviceProviderConfig, Doc, EmailConfig, GenericActionCtxWithAuthConfig, GroupConnectionDeprovisionMode, GroupConnectionPolicy, GroupConnectionPolicyPatch, HasDeviceProvider, HasPasskeyProvider, HasSSO, HasTotpProvider, HttpKeyContext, KeyDoc, KeyScope, OAuthMaterializedConfig, OAuthProfile, OAuthTokens, OIDCClaimMapping, PasskeyProviderConfig, PhoneConfig, SSOProviderConfig, ScopeChecker, TotpProviderConfig, UserOrderBy, UserWhere };
|
|
1160
|
+
export { AuthAuthorizationConfig, AuthGrant, AuthProviderConfig, AuthRoleId, ConvexAuthConfig, ConvexAuthMaterializedConfig, ConvexCredentialsConfig, CorsConfig, DeviceProviderConfig, Doc, EmailConfig, EmailUserConfig, GenericActionCtxWithAuthConfig, GenericDoc, GroupConnectionDeprovisionMode, GroupConnectionPolicy, GroupConnectionPolicyPatch, HasDeviceProvider, HasPasskeyProvider, HasSSO, HasTotpProvider, HttpKeyContext, KeyDoc, KeyScope, OAuthMaterializedConfig, OAuthProfile, OAuthTokens, OIDCClaimMapping, PasskeyProviderConfig, PhoneConfig, PhoneUserConfig, SSOProviderConfig, ScopeChecker, TotpProviderConfig, UserOrderBy, UserWhere };
|
|
1161
1161
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/server/users.js
CHANGED
|
@@ -2,7 +2,6 @@ import { LOG_LEVELS } from "../shared/log.js";
|
|
|
2
2
|
import { authDb } from "./db.js";
|
|
3
3
|
import { log } from "./log.js";
|
|
4
4
|
import { ConvexError } from "convex/values";
|
|
5
|
-
import { Effect, Match } from "effect";
|
|
6
5
|
|
|
7
6
|
//#region src/server/users.ts
|
|
8
7
|
function mergeExtend(existing, incoming) {
|
|
@@ -64,24 +63,19 @@ async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount
|
|
|
64
63
|
if (existingUserId === null) {
|
|
65
64
|
const existingUserWithVerifiedEmailId = typeof profile.email === "string" && shouldLinkViaEmail ? (await uniqueUserWithVerifiedEmail(ctx, profile.email, config))?._id ?? null : null;
|
|
66
65
|
const existingUserWithVerifiedPhoneId = typeof profile.phone === "string" && shouldLinkViaPhone ? (await uniqueUserWithVerifiedPhone(ctx, profile.phone, config))?._id ?? null : null;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return existingUserWithVerifiedEmailId$1;
|
|
78
|
-
})), Match.when({ tag: "phone" }, ({ existingUserWithVerifiedPhoneId: existingUserWithVerifiedPhoneId$1 }) => Effect.sync(() => {
|
|
79
|
-
log(LOG_LEVELS.DEBUG, `Found existing phone verified user, linking: ${existingUserWithVerifiedPhoneId$1}`);
|
|
80
|
-
return existingUserWithVerifiedPhoneId$1;
|
|
81
|
-
})), Match.when({ tag: "none" }, () => Effect.sync(() => {
|
|
66
|
+
if (existingUserWithVerifiedEmailId !== null && existingUserWithVerifiedPhoneId !== null) {
|
|
67
|
+
log(LOG_LEVELS.DEBUG, `Found existing email and phone verified users, so not linking: email: ${existingUserWithVerifiedEmailId}, phone: ${existingUserWithVerifiedPhoneId}`);
|
|
68
|
+
userId = null;
|
|
69
|
+
} else if (existingUserWithVerifiedEmailId !== null) {
|
|
70
|
+
log(LOG_LEVELS.DEBUG, `Found existing email verified user, linking: ${existingUserWithVerifiedEmailId}`);
|
|
71
|
+
userId = existingUserWithVerifiedEmailId;
|
|
72
|
+
} else if (existingUserWithVerifiedPhoneId !== null) {
|
|
73
|
+
log(LOG_LEVELS.DEBUG, `Found existing phone verified user, linking: ${existingUserWithVerifiedPhoneId}`);
|
|
74
|
+
userId = existingUserWithVerifiedPhoneId;
|
|
75
|
+
} else {
|
|
82
76
|
log(LOG_LEVELS.DEBUG, "No existing verified users found, creating new user");
|
|
83
|
-
|
|
84
|
-
}
|
|
77
|
+
userId = null;
|
|
78
|
+
}
|
|
85
79
|
if (userId !== null && config.sso?.hooks?.allowLink !== void 0 && (args.provider.type === "oauth" || args.provider.type === "sso")) {
|
|
86
80
|
if (await config.sso.hooks.allowLink({
|
|
87
81
|
protocol: args.provider.type === "oauth" && typeof args.accountExtend?.identity?.protocol === "string" ? args.accountExtend.identity.protocol : "oidc",
|
|
@@ -107,13 +101,14 @@ async function defaultCreateOrUpdateUser(ctx, existingSessionId, existingAccount
|
|
|
107
101
|
mode
|
|
108
102
|
});
|
|
109
103
|
if (Object.keys(patchData).length === 0) return userId;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
try {
|
|
105
|
+
await db.users.patch(currentUserId, patchData);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new ConvexError({
|
|
113
108
|
code: "USER_UPDATE_FAILED",
|
|
114
109
|
message: `Could not update user document with ID \`${currentUserId}\`, either the user has been deleted but their account has not, or the profile data doesn't match the \`users\` table schema: ${error instanceof Error ? error.message : String(error)}`
|
|
115
|
-
})
|
|
116
|
-
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
117
112
|
} else {
|
|
118
113
|
if (source === "login" && provisioningUser?.createOnSignIn === false) throw new ConvexError({
|
|
119
114
|
code: "NOT_AUTHORIZED",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/server/utils/cache.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create a synchronous TTL cache.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const jwksCache = createCache({
|
|
7
|
+
* capacity: 128,
|
|
8
|
+
* timeToLiveMs: 60 * 60 * 1000,
|
|
9
|
+
* lookup: (url) => createRemoteJWKSet(new URL(url)),
|
|
10
|
+
* });
|
|
11
|
+
* const jwks = jwksCache.get(url);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function createCache(opts) {
|
|
15
|
+
const { capacity, timeToLiveMs, lookup } = opts;
|
|
16
|
+
const store = /* @__PURE__ */ new Map();
|
|
17
|
+
function evictExpired() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
for (const [key, entry] of store) if (entry.expiresAt <= now) store.delete(key);
|
|
20
|
+
}
|
|
21
|
+
function evictOldest() {
|
|
22
|
+
if (store.size < capacity) return;
|
|
23
|
+
const firstKey = store.keys().next().value;
|
|
24
|
+
if (firstKey !== void 0) store.delete(firstKey);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
get(key) {
|
|
28
|
+
const existing = store.get(key);
|
|
29
|
+
if (existing && existing.expiresAt > Date.now()) return existing.value;
|
|
30
|
+
if (existing) store.delete(key);
|
|
31
|
+
evictExpired();
|
|
32
|
+
evictOldest();
|
|
33
|
+
const value = lookup(key);
|
|
34
|
+
store.set(key, {
|
|
35
|
+
value,
|
|
36
|
+
expiresAt: Date.now() + timeToLiveMs
|
|
37
|
+
});
|
|
38
|
+
return value;
|
|
39
|
+
},
|
|
40
|
+
invalidate(key) {
|
|
41
|
+
store.delete(key);
|
|
42
|
+
},
|
|
43
|
+
clear() {
|
|
44
|
+
store.clear();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { createCache };
|
|
51
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/server/utils/dispatch.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build a dispatch function from a handler record.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const run = createDispatch({
|
|
7
|
+
* signIn: (args) => handleSignIn(args),
|
|
8
|
+
* signOut: (args) => handleSignOut(args),
|
|
9
|
+
* });
|
|
10
|
+
* return await run("signIn", args);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
function createDispatch(handlers) {
|
|
14
|
+
const map = new Map(Object.entries(handlers));
|
|
15
|
+
return ((key, ...args) => {
|
|
16
|
+
const handler = map.get(key);
|
|
17
|
+
if (!handler) throw new Error(`Unknown dispatch key: "${key}"`);
|
|
18
|
+
return handler(...args);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compose multiple dispatch tables into one.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const dispatch = composeDispatch(coreHandlers, ssoHandlers);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function composeDispatch(a, b) {
|
|
29
|
+
return {
|
|
30
|
+
...a,
|
|
31
|
+
...b
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { composeDispatch, createDispatch };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/server/utils/retry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Retry `fn` with exponential backoff until it succeeds or retries are
|
|
4
|
+
* exhausted. On final failure the last error is re-thrown.
|
|
5
|
+
*/
|
|
6
|
+
async function retryWithBackoff(fn, opts = {}) {
|
|
7
|
+
const { maxRetries = 2, baseMs = 200, jitter = true } = opts;
|
|
8
|
+
let lastError;
|
|
9
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
10
|
+
return await fn();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
lastError = error;
|
|
13
|
+
if (attempt < maxRetries) {
|
|
14
|
+
const delay = baseMs * 2 ** attempt;
|
|
15
|
+
const jitterMs = jitter ? Math.random() * delay : 0;
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitterMs));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw lastError;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { retryWithBackoff };
|
|
24
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
//#region src/server/utils/span.ts
|
|
4
|
+
/**
|
|
5
|
+
* Thin wrapper around `@opentelemetry/api` tracing.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
const tracer = trace.getTracer("convex-auth");
|
|
10
|
+
/**
|
|
11
|
+
* Run `fn` inside an OpenTelemetry span.
|
|
12
|
+
*
|
|
13
|
+
* If `fn` throws, the span is marked as errored and the exception is
|
|
14
|
+
* recorded before re-throwing.
|
|
15
|
+
*/
|
|
16
|
+
async function withSpan(name, attributes, fn) {
|
|
17
|
+
return tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
18
|
+
try {
|
|
19
|
+
return await fn();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
22
|
+
if (error instanceof Error) span.recordException(error);
|
|
23
|
+
throw error;
|
|
24
|
+
} finally {
|
|
25
|
+
span.end();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { withSpan };
|
|
32
|
+
//# sourceMappingURL=span.js.map
|
package/dist/shared/errors.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { Data } from "effect";
|
|
2
|
-
|
|
3
1
|
//#region src/shared/errors.ts
|
|
4
|
-
var AuthFlowError = class extends
|
|
2
|
+
var AuthFlowError = class extends Error {
|
|
3
|
+
_tag = "AuthFlowError";
|
|
4
|
+
code;
|
|
5
|
+
constructor({ code, message }) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "AuthFlowError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
5
11
|
/** @internal */
|
|
6
12
|
const authFlowError = (code, message) => new AuthFlowError({
|
|
7
13
|
code,
|
package/dist/shared/log.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Cause, Effect, Match } from "effect";
|
|
2
|
-
|
|
3
1
|
//#region src/shared/log.ts
|
|
4
2
|
const LOG_LEVELS = {
|
|
5
3
|
ERROR: "ERROR",
|
|
@@ -18,28 +16,28 @@ function serialize(value) {
|
|
|
18
16
|
}
|
|
19
17
|
function logMessage(module, level, args, configuredLogLevel = "INFO") {
|
|
20
18
|
const message = args.map(serialize).join(" ");
|
|
21
|
-
|
|
19
|
+
const meta = {
|
|
22
20
|
module,
|
|
23
21
|
level
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
module
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
module
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
};
|
|
23
|
+
const handler = {
|
|
24
|
+
ERROR: () => {
|
|
25
|
+
console.error(`[${meta.module}] [${meta.level}]`, message);
|
|
26
|
+
},
|
|
27
|
+
WARN: () => {
|
|
28
|
+
if (configuredLogLevel === "ERROR") return;
|
|
29
|
+
console.warn(`[${meta.module}] [${meta.level}]`, message);
|
|
30
|
+
},
|
|
31
|
+
INFO: () => {
|
|
32
|
+
if (configuredLogLevel !== "INFO" && configuredLogLevel !== "DEBUG") return;
|
|
33
|
+
console.info(`[${meta.module}] [${meta.level}]`, message);
|
|
34
|
+
},
|
|
35
|
+
DEBUG: () => {
|
|
36
|
+
if (configuredLogLevel !== "DEBUG") return;
|
|
37
|
+
console.debug(`[${meta.module}] [${meta.level}]`, message);
|
|
38
|
+
}
|
|
39
|
+
}[level];
|
|
40
|
+
if (handler) handler();
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
//#endregion
|