@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.
Files changed (88) hide show
  1. package/README.md +3 -5
  2. package/dist/bin.js +6488 -1571
  3. package/dist/browser/index.js +10 -7
  4. package/dist/browser/locks.js +3 -5
  5. package/dist/browser/navigation.js +7 -10
  6. package/dist/browser/runtime.js +35 -33
  7. package/dist/client/core/types.js +17 -0
  8. package/dist/client/factors/device.js +26 -19
  9. package/dist/client/index.js +151 -163
  10. package/dist/client/runtime/proxy.js +6 -6
  11. package/dist/client/services/adapters.js +3 -7
  12. package/dist/client/services/http.js +2 -5
  13. package/dist/client/services/resolve.js +5 -11
  14. package/dist/client/services/runtime.js +2 -5
  15. package/dist/component/_generated/component.d.ts +46 -0
  16. package/dist/component/index.d.ts +3 -3
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/public/identity/sessions.js +38 -1
  19. package/dist/component/public/identity/tokens.js +81 -3
  20. package/dist/component/public/identity/verifiers.js +9 -3
  21. package/dist/component/public.js +3 -3
  22. package/dist/component/schema.d.ts +320 -320
  23. package/dist/core/index.d.ts +380 -0
  24. package/dist/core/index.js +83 -0
  25. package/dist/otel.d.ts +13 -17
  26. package/dist/otel.js +39 -49
  27. package/dist/providers/email.d.ts +2 -2
  28. package/dist/providers/password.js +8 -16
  29. package/dist/providers/phone.js +2 -9
  30. package/dist/server/auth-context.d.ts +204 -0
  31. package/dist/server/auth-context.js +76 -0
  32. package/dist/server/auth.d.ts +25 -187
  33. package/dist/server/auth.js +5 -96
  34. package/dist/server/componentContext.d.ts +12 -0
  35. package/dist/server/componentContext.js +1 -0
  36. package/dist/server/config.js +1 -12
  37. package/dist/server/constants.js +6 -0
  38. package/dist/server/contract.d.ts +1 -1
  39. package/dist/server/core.js +5 -14
  40. package/dist/server/crypto.js +26 -18
  41. package/dist/server/db.js +6 -1
  42. package/dist/server/device.js +88 -78
  43. package/dist/server/http.d.ts +4 -3
  44. package/dist/server/http.js +74 -86
  45. package/dist/server/index.d.ts +2 -1
  46. package/dist/server/limits.js +22 -15
  47. package/dist/server/mounts.d.ts +103 -103
  48. package/dist/server/mutations/account.js +6 -4
  49. package/dist/server/mutations/invalidate.js +3 -6
  50. package/dist/server/mutations/oauth.js +86 -88
  51. package/dist/server/mutations/refresh.js +45 -87
  52. package/dist/server/mutations/register.js +19 -19
  53. package/dist/server/mutations/retrieve.js +17 -15
  54. package/dist/server/mutations/signature.js +9 -13
  55. package/dist/server/mutations/signin.js +7 -3
  56. package/dist/server/mutations/signout.js +10 -15
  57. package/dist/server/mutations/store.js +22 -12
  58. package/dist/server/mutations/verifier.js +11 -6
  59. package/dist/server/mutations/verify.js +55 -46
  60. package/dist/server/oauth/runtime.js +27 -25
  61. package/dist/server/passkey.js +299 -250
  62. package/dist/server/prefetch.js +283 -281
  63. package/dist/server/refresh.js +7 -60
  64. package/dist/server/runtime.d.ts +82 -206
  65. package/dist/server/runtime.js +63 -56
  66. package/dist/server/services/config.js +5 -3
  67. package/dist/server/services/logger.js +2 -4
  68. package/dist/server/services/providers.js +2 -4
  69. package/dist/server/services/refresh.js +2 -4
  70. package/dist/server/services/resolve.js +15 -14
  71. package/dist/server/services/signin.js +2 -4
  72. package/dist/server/sessions.js +32 -33
  73. package/dist/server/signin.js +177 -142
  74. package/dist/server/sso/domain.d.ts +20 -68
  75. package/dist/server/sso/domain.js +444 -413
  76. package/dist/server/sso/http.js +53 -59
  77. package/dist/server/sso/oidc.js +94 -80
  78. package/dist/server/tokens.js +13 -3
  79. package/dist/server/totp.js +153 -116
  80. package/dist/server/types.d.ts +2 -2
  81. package/dist/server/users.js +18 -23
  82. package/dist/server/utils/cache.js +51 -0
  83. package/dist/server/utils/dispatch.js +36 -0
  84. package/dist/server/utils/retry.js +24 -0
  85. package/dist/server/utils/span.js +32 -0
  86. package/dist/shared/errors.js +9 -3
  87. package/dist/shared/log.js +20 -22
  88. package/package.json +41 -33
@@ -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
- const resolveTotpFlowFx = (params) => {
27
+ function resolveTotpFlow(params) {
30
28
  const flow = params.flow;
31
- return typeof flow === "string" && TOTP_FLOWS.includes(flow) ? Effect.succeed(flow) : Effect.fail(convexError("TOTP_MISSING_FLOW", "Missing `flow` parameter. Expected one of: setup, confirm, verify"));
32
- };
33
- const requireTotpVerifierFx = (verifier) => verifier != null ? Effect.succeed(verifier) : Effect.fail(convexError("TOTP_MISSING_VERIFIER", "Missing verifier for TOTP operation."));
34
- const requireTotpCodeFx = (params) => typeof params.code === "string" ? Effect.succeed(params.code) : Effect.fail(convexError("TOTP_MISSING_CODE", "Missing TOTP code."));
35
- const requireTotpIdFx = (params) => typeof params.totpId === "string" ? Effect.succeed(params.totpId) : Effect.fail(convexError("TOTP_MISSING_ID", "Missing TOTP enrollment ID."));
36
- const resolveTotpDispatchFx = (params, verifier) => Effect.flatMap(resolveTotpFlowFx(params), (flow) => Match.value(flow).pipe(Match.when("setup", () => Effect.succeed({
37
- flow: "setup",
38
- params
39
- })), Match.when("confirm", () => Effect.gen(function* () {
40
- const resolvedVerifier = yield* requireTotpVerifierFx(verifier);
41
- return {
42
- flow: "confirm",
43
- code: yield* requireTotpCodeFx(params),
44
- totpId: yield* requireTotpIdFx(params),
45
- verifier: resolvedVerifier
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
- })), Match.when("verify", () => Effect.gen(function* () {
48
- const resolvedVerifier = yield* requireTotpVerifierFx(verifier);
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: yield* requireTotpCodeFx(params),
62
+ code: requireTotpCode(params),
52
63
  verifier: resolvedVerifier
53
64
  };
54
- })), Match.exhaustive));
55
- const requireAuthenticatedUserId = (ctx) => Effect.flatMap(Effect.tryPromise({
56
- try: () => ctx.auth.getUserIdentity(),
57
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", String(error))
58
- }), (identity) => Match.value(identity).pipe(Match.when(null, () => Effect.fail(convexError("TOTP_AUTH_REQUIRED", "Sign in first, then set up two-factor authentication."))), Match.orElse((identity$1) => Effect.succeed(userIdFromIdentitySubject(identity$1.subject)))));
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 params = args.params ?? {};
62
- return Effect.flatMap(resolveTotpDispatchFx(params, args.verifier), (dispatch) => Match.value(dispatch).pipe(Match.when({ flow: "setup" }, ({ params: params$1 }) => Effect.gen(function* () {
63
- const userId = yield* requireAuthenticatedUserId(ctx);
64
- const secret = new Uint8Array(20);
65
- crypto.getRandomValues(secret);
66
- let accountName = params$1.accountName;
67
- if (!accountName) accountName = (yield* Effect.tryPromise({
68
- try: () => queryUserById(ctx, userId),
69
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`)
70
- }))?.email ?? "user";
71
- const uri = createTOTPKeyURI(provider.options.issuer, accountName, secret, provider.options.period, provider.options.digits);
72
- const base32Secret = encodeBase32LowerCaseNoPadding(secret);
73
- const verifier = yield* Effect.tryPromise({
74
- try: () => callVerifier(ctx),
75
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`)
76
- });
77
- yield* Effect.tryPromise({
78
- try: () => callVerifierSignature(ctx, {
79
- verifier,
80
- signature: JSON.stringify({
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
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`)
88
- });
89
- return {
90
- kind: "totpSetup",
91
- uri,
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 params$1.name === "string" ? params$1.name : void 0,
116
+ name: typeof setupParams.name === "string" ? setupParams.name : void 0,
102
117
  createdAt: Date.now()
103
- }),
104
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`)
105
- })
106
- };
107
- })), Match.when({ flow: "confirm" }, ({ code, totpId, verifier }) => Effect.gen(function* () {
108
- const userId = yield* requireAuthenticatedUserId(ctx);
109
- const doc = yield* Effect.tryPromise({
110
- try: () => queryTotpById(ctx, totpId),
111
- catch: () => convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.")
112
- });
113
- const totpDoc = yield* Match.value(doc).pipe(Match.when(null, () => Effect.fail(convexError("TOTP_NOT_FOUND", "TOTP enrollment not found."))), Match.orElse((doc$1) => Effect.succeed(doc$1)));
114
- if (totpDoc.verified) return yield* Effect.fail(convexError("TOTP_ALREADY_VERIFIED", "TOTP enrollment is already verified."));
115
- if (!verifyTOTPWithGracePeriod(new Uint8Array(totpDoc.secret), provider.options.period, provider.options.digits, code, 30)) return yield* Effect.fail(convexError("TOTP_INVALID_CODE", "Invalid TOTP code."));
116
- return {
117
- kind: "signedIn",
118
- signedIn: yield* Effect.tryPromise({
119
- try: async () => {
120
- await mutateTotpMarkVerified(ctx, totpId, Date.now());
121
- await mutateVerifierDelete(ctx, verifier);
122
- return callSignIn(ctx, {
123
- userId,
124
- generateTokens: true
125
- });
126
- },
127
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", String(error))
128
- })
129
- };
130
- })), Match.when({ flow: "verify" }, ({ code, verifier }) => Effect.gen(function* () {
131
- const doc = yield* Effect.tryPromise({
132
- try: () => queryVerifierById(ctx, verifier),
133
- catch: () => convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.")
134
- });
135
- const verifierDoc = yield* Match.value(doc).pipe(Match.when(null, () => Effect.fail(convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier."))), Match.orElse((doc$1) => Effect.succeed(doc$1)));
136
- const userId = JSON.parse(verifierDoc.signature).userId;
137
- const totp = yield* Effect.tryPromise({
138
- try: () => queryTotpVerifiedByUserId(ctx, userId),
139
- catch: () => convexError("TOTP_NO_ENROLLMENT", "No verified TOTP enrollment found.")
140
- });
141
- const totpDoc = yield* Match.value(totp).pipe(Match.when(null, () => Effect.fail(convexError("TOTP_NO_ENROLLMENT", "No verified TOTP enrollment found."))), Match.orElse((doc$1) => Effect.succeed(doc$1)));
142
- if (!verifyTOTPWithGracePeriod(new Uint8Array(totpDoc.secret), totpDoc.period, totpDoc.digits, code, 30)) return yield* Effect.fail(convexError("TOTP_INVALID_CODE", "Invalid TOTP code."));
143
- return {
144
- kind: "signedIn",
145
- signedIn: yield* Effect.tryPromise({
146
- try: async () => {
147
- await mutateTotpUpdateLastUsed(ctx, totpDoc._id, Date.now());
148
- await mutateVerifierDelete(ctx, verifier);
149
- return callSignIn(ctx, {
150
- userId,
151
- generateTokens: true
152
- });
153
- },
154
- catch: (error) => asConvexError(error, "INTERNAL_ERROR", String(error))
155
- })
156
- };
157
- })), Match.exhaustive));
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
@@ -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
@@ -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
- const linkDispatch = {
68
- tag: existingUserWithVerifiedEmailId !== null && existingUserWithVerifiedPhoneId !== null ? "both" : existingUserWithVerifiedEmailId !== null ? "email" : existingUserWithVerifiedPhoneId !== null ? "phone" : "none",
69
- existingUserWithVerifiedEmailId,
70
- existingUserWithVerifiedPhoneId
71
- };
72
- userId = await Effect.runPromise(Match.value(linkDispatch).pipe(Match.when({ tag: "both" }, ({ existingUserWithVerifiedEmailId: existingUserWithVerifiedEmailId$1, existingUserWithVerifiedPhoneId: existingUserWithVerifiedPhoneId$1 }) => Effect.sync(() => {
73
- log(LOG_LEVELS.DEBUG, `Found existing email and phone verified users, so not linking: email: ${existingUserWithVerifiedEmailId$1}, phone: ${existingUserWithVerifiedPhoneId$1}`);
74
- return null;
75
- })), Match.when({ tag: "email" }, ({ existingUserWithVerifiedEmailId: existingUserWithVerifiedEmailId$1 }) => Effect.sync(() => {
76
- log(LOG_LEVELS.DEBUG, `Found existing email verified user, linking: ${existingUserWithVerifiedEmailId$1}`);
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
- return null;
84
- })), Match.exhaustive));
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
- await Effect.runPromise(Effect.tryPromise({
111
- try: () => db.users.patch(currentUserId, patchData),
112
- catch: (error) => new ConvexError({
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
@@ -1,7 +1,13 @@
1
- import { Data } from "effect";
2
-
3
1
  //#region src/shared/errors.ts
4
- var AuthFlowError = class extends Data.TaggedError("AuthFlowError") {};
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,
@@ -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
- return Match.value(level).pipe(Match.when("ERROR", () => Effect.runSync(Effect.logError(message).pipe(Effect.annotateLogs({
19
+ const meta = {
22
20
  module,
23
21
  level
24
- })))), Match.when("WARN", () => {
25
- if (configuredLogLevel === "ERROR") return;
26
- return Effect.runSync(Effect.logWarning(message).pipe(Effect.annotateLogs({
27
- module,
28
- level
29
- })));
30
- }), Match.when("INFO", () => {
31
- if (configuredLogLevel !== "INFO" && configuredLogLevel !== "DEBUG") return;
32
- return Effect.runSync(Effect.logInfo(message).pipe(Effect.annotateLogs({
33
- module,
34
- level
35
- })));
36
- }), Match.when("DEBUG", () => {
37
- if (configuredLogLevel !== "DEBUG") return;
38
- return Effect.runSync(Effect.logDebug(message).pipe(Effect.annotateLogs({
39
- module,
40
- level
41
- })));
42
- }), Match.exhaustive);
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