@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
@@ -4,7 +4,6 @@ import { authDb } from "../db.js";
4
4
  import { log, maybeRedact } from "../log.js";
5
5
  import { AUTH_STORE_REF } from "./store/refs.js";
6
6
  import { ConvexError, v } from "convex/values";
7
- import { Effect, Match } from "effect";
8
7
 
9
8
  //#region src/server/mutations/account.ts
10
9
  const modifyAccountArgs = v.object({
@@ -14,7 +13,7 @@ const modifyAccountArgs = v.object({
14
13
  secret: v.string()
15
14
  })
16
15
  });
17
- function modifyAccountImpl(ctx, args, getProviderOrThrow, config) {
16
+ async function modifyAccountImpl(ctx, args, getProviderOrThrow, config) {
18
17
  const { provider, account } = args;
19
18
  const db = authDb(ctx, config);
20
19
  log(LOG_LEVELS.DEBUG, "modifyAccountImpl args:", {
@@ -24,10 +23,13 @@ function modifyAccountImpl(ctx, args, getProviderOrThrow, config) {
24
23
  secret: maybeRedact(account.secret ?? "")
25
24
  }
26
25
  });
27
- return Effect.flatMap(Effect.promise(() => db.accounts.get(provider, account.id)), (existingAccount) => Match.value(existingAccount).pipe(Match.when(null, () => Effect.fail(new ConvexError({
26
+ const existingAccount = await db.accounts.get(provider, account.id);
27
+ if (existingAccount === null) throw new ConvexError({
28
28
  code: "ACCOUNT_NOT_FOUND",
29
29
  message: `Cannot modify account with ID ${account.id} because it does not exist`
30
- }))), Match.orElse((existingAccount$1) => Effect.flatMap(hash(getProviderOrThrow(provider), account.secret), (hashedSecret) => Effect.promise(() => db.accounts.patch(existingAccount$1._id, { secret: hashedSecret }))))));
30
+ });
31
+ const hashedSecret = await hash(getProviderOrThrow(provider), account.secret);
32
+ await db.accounts.patch(existingAccount._id, { secret: hashedSecret });
31
33
  }
32
34
  const callModifyAccount = async (ctx, args) => {
33
35
  return ctx.runMutation(AUTH_STORE_REF, { args: {
@@ -4,7 +4,6 @@ import { log } from "../log.js";
4
4
  import { AUTH_STORE_REF } from "./store/refs.js";
5
5
  import { deleteSession } from "../sessions.js";
6
6
  import { v } from "convex/values";
7
- import { Effect } from "effect";
8
7
 
9
8
  //#region src/server/mutations/invalidate.ts
10
9
  const invalidateSessionsArgs = v.object({
@@ -17,15 +16,13 @@ const callInvalidateSessions = async (ctx, args) => {
17
16
  ...args
18
17
  } });
19
18
  };
20
- function invalidateSessionsImpl(ctx, args, config) {
19
+ async function invalidateSessionsImpl(ctx, args, config) {
21
20
  log(LOG_LEVELS.DEBUG, "invalidateSessionsImpl args:", args);
22
21
  const { userId, except } = args;
23
22
  const exceptSet = new Set(except ?? []);
24
23
  const typedUserId = userId;
25
- return Effect.gen(function* () {
26
- const sessions = yield* Effect.promise(() => authDb(ctx, config).sessions.listByUser(typedUserId));
27
- yield* Effect.forEach(sessions, (session) => exceptSet.has(session._id) ? Effect.void : Effect.promise(() => deleteSession(ctx, session, config)), { discard: true });
28
- });
24
+ const sessions = await authDb(ctx, config).sessions.listByUser(typedUserId);
25
+ await Promise.all(sessions.map((session) => exceptSet.has(session._id) ? Promise.resolve() : deleteSession(ctx, session, config)));
29
26
  }
30
27
 
31
28
  //#endregion
@@ -9,7 +9,6 @@ import { GROUP_OIDC_PROVIDER_PREFIX, GROUP_SAML_PROVIDER_PREFIX, isGroupProvider
9
9
  import { createSyntheticOAuthMaterializedConfig } from "../sso/oidc.js";
10
10
  import { normalizeGroupConnectionPolicy, resolveProvisionedRoleIds } from "../sso/policy.js";
11
11
  import { ConvexError, v } from "convex/values";
12
- import { Effect } from "effect";
13
12
 
14
13
  //#region src/server/mutations/oauth.ts
15
14
  const OAUTH_SIGN_IN_EXPIRATION_MS = 1e3 * 60 * 2;
@@ -44,106 +43,105 @@ function normalizeAccountExtend(provider, providerAccountId, accountExtend) {
44
43
  }
45
44
  };
46
45
  }
47
- function userOAuthImpl(ctx, args, getProviderOrThrow, config) {
46
+ async function userOAuthImpl(ctx, args, getProviderOrThrow, config) {
48
47
  log("DEBUG", "userOAuthImpl args:", args);
49
48
  const { profile, provider, providerAccountId, signature, accountExtend } = args;
50
49
  const typedProfile = profile;
51
50
  const db = authDb(ctx, config);
52
51
  const connectionId = provider.startsWith(GROUP_OIDC_PROVIDER_PREFIX) ? provider.slice(GROUP_OIDC_PROVIDER_PREFIX.length) : provider.startsWith(GROUP_SAML_PROVIDER_PREFIX) ? provider.slice(GROUP_SAML_PROVIDER_PREFIX.length) : null;
53
52
  const connectionProtocol = provider.startsWith(GROUP_OIDC_PROVIDER_PREFIX) ? "oidc" : provider.startsWith(GROUP_SAML_PROVIDER_PREFIX) ? "saml" : null;
54
- return Effect.gen(function* () {
55
- const existingAccount = yield* Effect.promise(() => db.accounts.get(provider, providerAccountId));
56
- const connection = connectionId !== null ? yield* Effect.promise(() => getGroupConnection(ctx, config.component.public, connectionId)) : null;
57
- const group = connection !== null ? yield* Effect.promise(() => getGroup(ctx, config.component.public, connection.groupId)) : null;
58
- const connectionPolicy = connection ? normalizeGroupConnectionPolicy(group?.policy) : null;
59
- const existingScimIdentity = connectionId !== null && existingAccount === null && connectionPolicy?.provisioning.scimReuse.user === "externalId" ? yield* Effect.promise(() => ctx.runQuery(config.component.public.groupConnectionScimIdentityGet, {
60
- connectionId,
61
- resourceType: "user",
62
- externalId: providerAccountId
63
- })) : null;
64
- const verifier = yield* Effect.tryPromise({
65
- try: () => db.verifiers.getBySignature(signature),
66
- catch: () => new ConvexError({
67
- code: "OAUTH_INVALID_STATE",
68
- message: "Invalid OAuth state. Please try signing in again."
69
- })
70
- }).pipe(Effect.flatMap((verifier$1) => verifier$1 === null ? Effect.fail(new ConvexError({
53
+ const existingAccount = await db.accounts.get(provider, providerAccountId);
54
+ const connection = connectionId !== null ? await getGroupConnection(ctx, config.component.public, connectionId) : null;
55
+ const group = connection !== null ? await getGroup(ctx, config.component.public, connection.groupId) : null;
56
+ const connectionPolicy = connection ? normalizeGroupConnectionPolicy(group?.policy) : null;
57
+ const existingScimIdentity = connectionId !== null && existingAccount === null && connectionPolicy?.provisioning.scimReuse.user === "externalId" ? await ctx.runQuery(config.component.public.groupConnectionScimIdentityGet, {
58
+ connectionId,
59
+ resourceType: "user",
60
+ externalId: providerAccountId
61
+ }) : null;
62
+ let verifier;
63
+ try {
64
+ verifier = await db.verifiers.getBySignature(signature);
65
+ } catch {
66
+ throw new ConvexError({
71
67
  code: "OAUTH_INVALID_STATE",
72
68
  message: "Invalid OAuth state. Please try signing in again."
73
- })) : Effect.succeed(verifier$1)));
74
- const profileResolved = (yield* Effect.promise(async () => config.sso?.hooks?.profileResolved ? await config.sso.hooks.profileResolved({
75
- protocol: connectionProtocol ?? "oidc",
76
- connectionId: connectionId ?? void 0,
77
- profile: typedProfile
78
- }) : void 0)) ?? typedProfile;
79
- const profileForProvisioning = (yield* Effect.promise(async () => config.sso?.hooks?.beforeProvision ? await config.sso.hooks.beforeProvision({
80
- protocol: connectionProtocol ?? "oidc",
81
- connectionId: connectionId ?? void 0,
82
- profile: profileResolved
83
- }) : void 0)) ?? profileResolved;
84
- const { accountId } = yield* Effect.promise(() => upsertUserAndAccount(ctx, verifier.sessionId ?? null, existingAccount !== null ? { existingAccount } : { providerAccountId }, {
85
- type: "oauth",
86
- provider: isGroupProviderId(provider) ? createSyntheticOAuthMaterializedConfig(provider, { accountLinking: connectionProtocol === "oidc" ? connectionPolicy?.identity.accountLinking.oidc : connectionProtocol === "saml" ? connectionPolicy?.identity.accountLinking.saml : void 0 }) : getProviderOrThrow(provider),
87
- profile: profileForProvisioning,
88
- accountExtend: normalizeAccountExtend(provider, providerAccountId, accountExtend)
89
- }, config, connectionPolicy?.provisioning.user ? {
90
- existingUserId: existingScimIdentity?.userId,
91
- provisioningUser: connectionPolicy.provisioning.user,
92
- source: "login"
93
- } : existingScimIdentity?.userId ? { existingUserId: existingScimIdentity.userId } : void 0));
94
- if (connectionId !== null && connectionPolicy?.provisioning.jit.mode === "createUserAndMembership") {
95
- const userId = (yield* Effect.promise(() => db.accounts.getById(accountId)))?.userId;
96
- if (userId) {
97
- const groupId = connection?.groupId;
98
- if (groupId) {
99
- const provisionedRoleIds = resolveProvisionedRoleIds({
100
- policy: connectionPolicy,
101
- groups: Array.isArray(typedProfile.groups) ? typedProfile.groups : void 0,
102
- roles: Array.isArray(typedProfile.roles) ? typedProfile.roles : void 0
103
- });
104
- const existingMembership = yield* Effect.promise(() => ctx.runQuery(config.component.public.memberGetByGroupAndUser, {
105
- userId,
106
- groupId
107
- }));
108
- if (existingMembership === null) yield* Effect.promise(() => ctx.runMutation(config.component.public.memberAdd, {
109
- groupId,
110
- userId,
111
- roleIds: provisionedRoleIds,
112
- status: "active"
113
- }));
114
- else if (provisionedRoleIds.length > 0) yield* Effect.promise(() => ctx.runMutation(config.component.public.memberUpdate, {
115
- memberId: existingMembership._id,
116
- data: { roleIds: provisionedRoleIds }
117
- }));
118
- }
69
+ });
70
+ }
71
+ if (verifier === null) throw new ConvexError({
72
+ code: "OAUTH_INVALID_STATE",
73
+ message: "Invalid OAuth state. Please try signing in again."
74
+ });
75
+ const profileResolved = (config.sso?.hooks?.profileResolved ? await config.sso.hooks.profileResolved({
76
+ protocol: connectionProtocol ?? "oidc",
77
+ connectionId: connectionId ?? void 0,
78
+ profile: typedProfile
79
+ }) : void 0) ?? typedProfile;
80
+ const profileForProvisioning = (config.sso?.hooks?.beforeProvision ? await config.sso.hooks.beforeProvision({
81
+ protocol: connectionProtocol ?? "oidc",
82
+ connectionId: connectionId ?? void 0,
83
+ profile: profileResolved
84
+ }) : void 0) ?? profileResolved;
85
+ const { accountId } = await upsertUserAndAccount(ctx, verifier.sessionId ?? null, existingAccount !== null ? { existingAccount } : { providerAccountId }, {
86
+ type: "oauth",
87
+ provider: isGroupProviderId(provider) ? createSyntheticOAuthMaterializedConfig(provider, { accountLinking: connectionProtocol === "oidc" ? connectionPolicy?.identity.accountLinking.oidc : connectionProtocol === "saml" ? connectionPolicy?.identity.accountLinking.saml : void 0 }) : getProviderOrThrow(provider),
88
+ profile: profileForProvisioning,
89
+ accountExtend: normalizeAccountExtend(provider, providerAccountId, accountExtend)
90
+ }, config, connectionPolicy?.provisioning.user ? {
91
+ existingUserId: existingScimIdentity?.userId,
92
+ provisioningUser: connectionPolicy.provisioning.user,
93
+ source: "login"
94
+ } : existingScimIdentity?.userId ? { existingUserId: existingScimIdentity.userId } : void 0);
95
+ if (connectionId !== null && connectionPolicy?.provisioning.jit.mode === "createUserAndMembership") {
96
+ const userId = (await db.accounts.getById(accountId))?.userId;
97
+ if (userId) {
98
+ const groupId = connection?.groupId;
99
+ if (groupId) {
100
+ const provisionedRoleIds = resolveProvisionedRoleIds({
101
+ policy: connectionPolicy,
102
+ groups: Array.isArray(typedProfile.groups) ? typedProfile.groups : void 0,
103
+ roles: Array.isArray(typedProfile.roles) ? typedProfile.roles : void 0
104
+ });
105
+ const existingMembership = await ctx.runQuery(config.component.public.memberGetByGroupAndUser, {
106
+ userId,
107
+ groupId
108
+ });
109
+ if (existingMembership === null) await ctx.runMutation(config.component.public.memberAdd, {
110
+ groupId,
111
+ userId,
112
+ roleIds: provisionedRoleIds,
113
+ status: "active"
114
+ });
115
+ else if (provisionedRoleIds.length > 0) await ctx.runMutation(config.component.public.memberUpdate, {
116
+ memberId: existingMembership._id,
117
+ data: { roleIds: provisionedRoleIds }
118
+ });
119
119
  }
120
120
  }
121
- if (connectionId !== null) {
122
- const userId = (yield* Effect.promise(() => db.accounts.getById(accountId)))?.userId;
123
- if (userId) yield* Effect.promise(async () => {
124
- if (config.sso?.hooks?.afterProvision) await config.sso.hooks.afterProvision({
125
- protocol: connectionProtocol ?? "oidc",
126
- connectionId,
127
- profile: profileForProvisioning,
128
- userId
129
- });
121
+ }
122
+ if (connectionId !== null) {
123
+ const userId = (await db.accounts.getById(accountId))?.userId;
124
+ if (userId) {
125
+ if (config.sso?.hooks?.afterProvision) await config.sso.hooks.afterProvision({
126
+ protocol: connectionProtocol ?? "oidc",
127
+ connectionId,
128
+ profile: profileForProvisioning,
129
+ userId
130
130
  });
131
131
  }
132
- const code = generateRandomString(8, "0123456789");
133
- yield* Effect.promise(() => db.verifiers.delete(verifier._id));
134
- const existingVerificationCode = yield* Effect.promise(() => db.verificationCodes.getByAccountId(accountId));
135
- if (existingVerificationCode !== null) yield* Effect.promise(() => db.verificationCodes.delete(existingVerificationCode._id));
136
- yield* Effect.promise(async () => {
137
- await db.verificationCodes.create({
138
- code: await sha256(code),
139
- accountId,
140
- provider,
141
- expirationTime: Date.now() + OAUTH_SIGN_IN_EXPIRATION_MS,
142
- verifier: verifier._id
143
- });
144
- });
145
- return code;
132
+ }
133
+ const code = generateRandomString(8, "0123456789");
134
+ await db.verifiers.delete(verifier._id);
135
+ const existingVerificationCode = await db.verificationCodes.getByAccountId(accountId);
136
+ if (existingVerificationCode !== null) await db.verificationCodes.delete(existingVerificationCode._id);
137
+ await db.verificationCodes.create({
138
+ code: await sha256(code),
139
+ accountId,
140
+ provider,
141
+ expirationTime: Date.now() + OAUTH_SIGN_IN_EXPIRATION_MS,
142
+ verifier: verifier._id
146
143
  });
144
+ return code;
147
145
  }
148
146
  const callUserOAuth = async (ctx, args) => {
149
147
  return ctx.runMutation(AUTH_STORE_REF, { args: {
@@ -1,104 +1,62 @@
1
1
  import { authDb } from "../db.js";
2
2
  import { log, maybeRedact } from "../log.js";
3
3
  import { AUTH_STORE_REF } from "./store/refs.js";
4
- import { REFRESH_TOKEN_REUSE_WINDOW_MS, invalidateRefreshTokensInSubtree, parseRefreshToken, refreshTokenIfValid } from "../refresh.js";
4
+ import { REFRESH_TOKEN_REUSE_WINDOW_MS, parseRefreshToken, refreshTokenExpirationTime } from "../refresh.js";
5
5
  import { generateTokensForSession } from "../sessions.js";
6
+ import { withSpan } from "../utils/span.js";
6
7
  import { v } from "convex/values";
7
- import { Data, Effect, Match } from "effect";
8
8
 
9
9
  //#region src/server/mutations/refresh.ts
10
- const asSessionId = (id) => id;
11
- const asRefreshTokenId = (id) => id;
12
10
  const refreshSessionArgs = v.object({ refreshToken: v.string() });
13
- var RefreshFailure = class extends Data.TaggedError("RefreshFailure") {};
14
- const softTry = (try_, reason) => Effect.tryPromise({
15
- try: try_,
16
- catch: () => new RefreshFailure({ reason })
17
- });
18
- const softTryEffect = (effect, reason) => effect.pipe(Effect.mapError(() => new RefreshFailure({ reason })));
19
- const softCleanup = (effect) => effect.pipe(Effect.catchTag("RefreshFailure", (failure) => Effect.sync(() => {
20
- log("DEBUG", failure.reason);
21
- })), Effect.asVoid);
22
- function refreshSessionImpl(ctx, args, _getProviderOrThrow, config) {
11
+ async function refreshSessionImpl(ctx, args, config) {
23
12
  const db = authDb(ctx, config);
24
- const { refreshToken } = args;
25
- return Effect.gen(function* () {
26
- const { refreshTokenId, sessionId: tokenSessionId } = yield* parseRefreshToken(refreshToken).pipe(Effect.mapError((error) => new RefreshFailure({ reason: error.data.message })));
27
- yield* Effect.sync(() => {
28
- log("DEBUG", `refreshSessionImpl args: Token ID: ${maybeRedact(refreshTokenId)} Session ID: ${maybeRedact(tokenSessionId)}`);
29
- });
30
- const validationResult = yield* refreshTokenIfValid(ctx, refreshTokenId, tokenSessionId, config);
31
- if (validationResult === null) {
32
- yield* softCleanup(softTry(async () => {
33
- const session$1 = await db.sessions.getById(asSessionId(tokenSessionId));
34
- if (session$1 !== null) await db.sessions.delete(session$1._id);
35
- }, "Skipping invalid session id during refresh cleanup"));
36
- yield* softCleanup(softTry(() => authDb(ctx, config).refreshTokens.deleteAll(asSessionId(tokenSessionId)), "Skipping invalid token session id during refresh token cleanup"));
37
- return null;
38
- }
39
- const { session, refreshTokenDoc } = validationResult;
40
- const sessionId = session._id;
41
- const userId = session.userId;
42
- const tokenFirstUsed = refreshTokenDoc.firstUsedTime;
43
- const tokenDispatch = tokenFirstUsed === void 0 ? { tag: "firstUse" } : {
44
- tag: "reuse",
45
- tokenFirstUsed
46
- };
47
- return yield* Match.value(tokenDispatch).pipe(Match.when({ tag: "firstUse" }, () => softTryEffect(Effect.gen(function* () {
48
- yield* Effect.promise(() => db.refreshTokens.patch(asRefreshTokenId(refreshTokenId), { firstUsedTime: Date.now() }));
49
- const result = yield* Effect.promise(() => generateTokensForSession(ctx, config, {
50
- userId,
51
- sessionId,
52
- issuedRefreshTokenId: null,
53
- parentRefreshTokenId: asRefreshTokenId(refreshTokenId)
54
- }));
55
- const { refreshTokenId: newRefreshTokenId } = yield* parseRefreshToken(result.refreshToken).pipe(Effect.mapError((error) => new RefreshFailure({ reason: error.data.message })));
56
- yield* Effect.sync(() => {
57
- log("DEBUG", `Exchanged ${maybeRedact(refreshTokenDoc._id)} (first use) for new refresh token ${maybeRedact(newRefreshTokenId)}`);
58
- });
59
- return result;
60
- }), "Failed during first-use token exchange")), Match.when({ tag: "reuse" }, ({ tokenFirstUsed: tokenFirstUsed$1 }) => softTry(() => authDb(ctx, config).refreshTokens.getActive(asSessionId(tokenSessionId)), "Failed to load active refresh token").pipe(Effect.flatMap((activeRefreshToken) => {
61
- log("DEBUG", `Active refresh token: ${maybeRedact(activeRefreshToken?._id ?? "(none)")}, parent ${maybeRedact(activeRefreshToken?.parentRefreshTokenId ?? "(none)")}`);
62
- const reuseDispatch = activeRefreshToken !== null && activeRefreshToken.parentRefreshTokenId === refreshTokenId ? {
63
- tag: "parentOfActive",
64
- activeRefreshToken
65
- } : tokenFirstUsed$1 + REFRESH_TOKEN_REUSE_WINDOW_MS > Date.now() ? { tag: "withinReuseWindow" } : { tag: "outsideReuseWindow" };
66
- return Match.value(reuseDispatch).pipe(Match.when({ tag: "parentOfActive" }, ({ activeRefreshToken: activeRefreshToken$1 }) => softTry(() => generateTokensForSession(ctx, config, {
67
- userId,
68
- sessionId,
69
- issuedRefreshTokenId: activeRefreshToken$1._id,
70
- parentRefreshTokenId: asRefreshTokenId(refreshTokenId)
71
- }), "Failed to generate tokens for parent reuse").pipe(Effect.tap(() => Effect.sync(() => {
72
- log("DEBUG", `Token ${maybeRedact(refreshTokenDoc._id)} is parent of active refresh token ${maybeRedact(activeRefreshToken$1._id)}, so returning that token`);
73
- })))), Match.when({ tag: "withinReuseWindow" }, () => softTryEffect(Effect.gen(function* () {
74
- const result = yield* Effect.promise(() => generateTokensForSession(ctx, config, {
75
- userId,
13
+ return withSpan("convex-auth.refresh.session", { hasRefreshToken: true }, async () => {
14
+ try {
15
+ let refreshTokenId;
16
+ let sessionId;
17
+ try {
18
+ ({refreshTokenId, sessionId} = parseRefreshToken(args.refreshToken));
19
+ } catch {
20
+ throw new RefreshFailure("Failed to parse refresh token");
21
+ }
22
+ log("DEBUG", `refreshSessionImpl args: Token ID: ${maybeRedact(refreshTokenId)} Session ID: ${maybeRedact(sessionId)}`);
23
+ let exchanged;
24
+ try {
25
+ exchanged = await db.refreshTokens.exchange({
26
+ refreshTokenId,
76
27
  sessionId,
77
- issuedRefreshTokenId: null,
78
- parentRefreshTokenId: asRefreshTokenId(refreshTokenId)
79
- }));
80
- const { refreshTokenId: newRefreshTokenId } = yield* parseRefreshToken(result.refreshToken).pipe(Effect.mapError((error) => new RefreshFailure({ reason: error.data.message })));
81
- yield* Effect.sync(() => {
82
- log("DEBUG", `Exchanged ${maybeRedact(refreshTokenDoc._id)} (reuse) for new refresh token ${maybeRedact(newRefreshTokenId)}`);
83
- });
84
- return result;
85
- }), "Failed to generate tokens for reuse window")), Match.when({ tag: "outsideReuseWindow" }, () => softTryEffect(Effect.gen(function* () {
86
- yield* Effect.sync(() => {
87
- log("ERROR", "Refresh token used outside of reuse window");
88
- log("DEBUG", `Token ${maybeRedact(refreshTokenDoc._id)} being used outside of reuse window, so invalidating all refresh tokens in subtree`);
28
+ now: Date.now(),
29
+ refreshTokenExpirationTime: refreshTokenExpirationTime(config),
30
+ reuseWindowMs: REFRESH_TOKEN_REUSE_WINDOW_MS
89
31
  });
90
- const tokensToInvalidate = yield* invalidateRefreshTokensInSubtree(ctx, refreshTokenDoc, config);
91
- yield* Effect.sync(() => {
92
- log("DEBUG", `Invalidated ${tokensToInvalidate.length} refresh tokens in subtree: ${tokensToInvalidate.map((token) => maybeRedact(token._id)).join(", ")}`);
32
+ } catch {
33
+ throw new RefreshFailure("Failed to exchange refresh token");
34
+ }
35
+ if (exchanged === null) return null;
36
+ try {
37
+ return await generateTokensForSession(config, {
38
+ userId: exchanged.userId,
39
+ sessionId: exchanged.sessionId,
40
+ refreshTokenId: exchanged.refreshTokenId
93
41
  });
42
+ } catch {
43
+ throw new RefreshFailure("Failed to generate refresh-session tokens");
44
+ }
45
+ } catch (e) {
46
+ if (e instanceof RefreshFailure) {
47
+ log("DEBUG", e.reason);
94
48
  return null;
95
- }), "Failed to invalidate refresh tokens in subtree")), Match.exhaustive);
96
- }))), Match.exhaustive);
97
- }).pipe(Effect.withSpan("convex-auth.refresh.session", { attributes: { hasRefreshToken: true } }), Effect.catch((failure) => Effect.sync(() => {
98
- log("DEBUG", failure.reason);
99
- return null;
100
- })));
49
+ }
50
+ throw e;
51
+ }
52
+ });
101
53
  }
54
+ var RefreshFailure = class extends Error {
55
+ constructor(reason) {
56
+ super(reason);
57
+ this.reason = reason;
58
+ }
59
+ };
102
60
  const callRefreshSession = async (ctx, args) => {
103
61
  return ctx.runMutation(AUTH_STORE_REF, { args: {
104
62
  type: "refreshSession",
@@ -7,7 +7,6 @@ import { getAuthSessionId } from "../sessions.js";
7
7
  import { upsertUserAndAccount } from "../users.js";
8
8
  import { payloadRecordValidator } from "../payloads.js";
9
9
  import { ConvexError, v } from "convex/values";
10
- import { Effect, Match } from "effect";
11
10
 
12
11
  //#region src/server/mutations/register.ts
13
12
  const createAccountFromCredentialsArgs = v.object({
@@ -20,7 +19,7 @@ const createAccountFromCredentialsArgs = v.object({
20
19
  shouldLinkViaEmail: v.optional(v.boolean()),
21
20
  shouldLinkViaPhone: v.optional(v.boolean())
22
21
  });
23
- function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config) {
22
+ async function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config) {
24
23
  log(LOG_LEVELS.DEBUG, "createAccountFromCredentialsImpl args:", {
25
24
  provider: args.provider,
26
25
  account: {
@@ -32,10 +31,11 @@ function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config)
32
31
  const db = authDb(ctx, config);
33
32
  const provider = getProviderOrThrow(providerId);
34
33
  const typedProfile = profile;
35
- return Effect.flatMap(Effect.promise(() => db.accounts.get(provider.id, account.id)), (existingAccount) => Match.value(existingAccount).pipe(Match.when(null, () => Effect.gen(function* () {
34
+ const existingAccount = await db.accounts.get(provider.id, account.id);
35
+ if (existingAccount === null) {
36
36
  const accountSecret = account.secret;
37
- const secret = accountSecret === void 0 ? void 0 : yield* hash(provider, accountSecret);
38
- const { userId, accountId } = yield* Effect.promise(async () => upsertUserAndAccount(ctx, await getAuthSessionId(ctx), {
37
+ const secret = accountSecret === void 0 ? void 0 : await hash(provider, accountSecret);
38
+ const { userId, accountId } = await upsertUserAndAccount(ctx, await getAuthSessionId(ctx), {
39
39
  providerAccountId: account.id,
40
40
  secret
41
41
  }, {
@@ -44,38 +44,38 @@ function createAccountFromCredentialsImpl(ctx, args, getProviderOrThrow, config)
44
44
  profile: typedProfile,
45
45
  shouldLinkViaEmail,
46
46
  shouldLinkViaPhone
47
- }, config));
48
- const [createdAccount, createdUser] = yield* Effect.all([Effect.promise(() => db.accounts.getById(accountId)), Effect.promise(() => db.users.getById(userId))]);
49
- if (createdAccount === null) return yield* Effect.fail(new ConvexError({
47
+ }, config);
48
+ const [createdAccount, createdUser] = await Promise.all([db.accounts.getById(accountId), db.users.getById(userId)]);
49
+ if (createdAccount === null) throw new ConvexError({
50
50
  code: "ACCOUNT_NOT_FOUND",
51
51
  message: "Created account was not found."
52
- }));
53
- if (createdUser === null) return yield* Effect.fail(new ConvexError({
52
+ });
53
+ if (createdUser === null) throw new ConvexError({
54
54
  code: "USER_UPDATE_FAILED",
55
55
  message: "Created user was not found."
56
- }));
56
+ });
57
57
  return {
58
58
  account: createdAccount,
59
59
  user: createdUser
60
60
  };
61
- })), Match.orElse((existingAccount$1) => Effect.gen(function* () {
61
+ } else {
62
62
  if (account.secret !== void 0) {
63
63
  const accountSecret = account.secret;
64
- if (!(yield* verify(provider, accountSecret, existingAccount$1.secret ?? ""))) return yield* Effect.fail(new ConvexError({
64
+ if (!await verify(provider, accountSecret, existingAccount.secret ?? "")) throw new ConvexError({
65
65
  code: "INVALID_CREDENTIALS",
66
66
  message: "Invalid credentials."
67
- }));
67
+ });
68
68
  }
69
- const user = yield* Effect.promise(() => db.users.getById(existingAccount$1.userId));
70
- if (user === null) return yield* Effect.fail(new ConvexError({
69
+ const user = await db.users.getById(existingAccount.userId);
70
+ if (user === null) throw new ConvexError({
71
71
  code: "ACCOUNT_NOT_FOUND",
72
72
  message: `Linked user for account ${account.id} was not found.`
73
- }));
73
+ });
74
74
  return {
75
- account: existingAccount$1,
75
+ account: existingAccount,
76
76
  user
77
77
  };
78
- }))));
78
+ }
79
79
  }
80
80
  const callCreateAccountFromCredentials = async (ctx, args) => {
81
81
  return ctx.runMutation(AUTH_STORE_REF, { args: {
@@ -5,7 +5,6 @@ import { log, maybeRedact } from "../log.js";
5
5
  import { AUTH_STORE_REF } from "./store/refs.js";
6
6
  import { isSignInRateLimited, recordFailedSignIn, resetSignInRateLimit } from "../limits.js";
7
7
  import { v } from "convex/values";
8
- import { Effect, Match } from "effect";
9
8
 
10
9
  //#region src/server/mutations/retrieve.ts
11
10
  const retrieveAccountWithCredentialsArgs = v.object({
@@ -15,7 +14,7 @@ const retrieveAccountWithCredentialsArgs = v.object({
15
14
  secret: v.optional(v.string())
16
15
  })
17
16
  });
18
- function retrieveAccountWithCredentialsImpl(ctx, args, getProviderOrThrow, config) {
17
+ async function retrieveAccountWithCredentialsImpl(ctx, args, getProviderOrThrow, config) {
19
18
  const { provider: providerId, account } = args;
20
19
  const db = authDb(ctx, config);
21
20
  log(LOG_LEVELS.DEBUG, "retrieveAccountWithCredentialsImpl args:", {
@@ -25,27 +24,30 @@ function retrieveAccountWithCredentialsImpl(ctx, args, getProviderOrThrow, confi
25
24
  secret: maybeRedact(account.secret ?? "")
26
25
  }
27
26
  });
28
- return Effect.catch(Effect.gen(function* () {
29
- const existingAccount = yield* Effect.promise(() => db.accounts.get(providerId, account.id));
27
+ try {
28
+ const existingAccount = await db.accounts.get(providerId, account.id);
30
29
  if (existingAccount === null) return "InvalidAccountId";
31
30
  if (account.secret !== void 0) {
32
31
  const accountSecret = account.secret;
33
- if (yield* isSignInRateLimited(ctx, existingAccount._id, config)) return "TooManyFailedAttempts";
34
- if (!(yield* verify(getProviderOrThrow(providerId), accountSecret, existingAccount.secret ?? ""))) {
35
- yield* recordFailedSignIn(ctx, existingAccount._id, config);
32
+ if (await isSignInRateLimited(ctx, existingAccount._id, config)) return "TooManyFailedAttempts";
33
+ if (!await verify(getProviderOrThrow(providerId), accountSecret, existingAccount.secret ?? "")) {
34
+ await recordFailedSignIn(ctx, existingAccount._id, config);
36
35
  return "InvalidSecret";
37
36
  }
38
- yield* resetSignInRateLimit(ctx, existingAccount._id, config);
37
+ await resetSignInRateLimit(ctx, existingAccount._id, config);
39
38
  }
40
- const user = yield* Effect.promise(() => db.users.getById(existingAccount.userId));
41
- return yield* Match.value(user).pipe(Match.when(null, () => {
39
+ const user = await db.users.getById(existingAccount.userId);
40
+ if (user === null) {
42
41
  log(LOG_LEVELS.ERROR, `Account ${existingAccount._id} is linked to missing user ${existingAccount.userId}`);
43
- return Effect.succeed("InvalidAccountId");
44
- }), Match.orElse((user$1) => Effect.succeed({
42
+ return "InvalidAccountId";
43
+ }
44
+ return {
45
45
  account: existingAccount,
46
- user: user$1
47
- })));
48
- }), () => Effect.succeed("InvalidAccountId"));
46
+ user
47
+ };
48
+ } catch {
49
+ return "InvalidAccountId";
50
+ }
49
51
  }
50
52
  const callRetrieveAccountWithCredentials = async (ctx, args) => {
51
53
  return ctx.runMutation(AUTH_STORE_REF, { args: {
@@ -1,31 +1,27 @@
1
1
  import { authDb } from "../db.js";
2
2
  import { AUTH_STORE_REF } from "./store/refs.js";
3
3
  import { ConvexError, v } from "convex/values";
4
- import { Effect, Option, pipe } from "effect";
5
4
 
6
5
  //#region src/server/mutations/signature.ts
7
6
  const verifierSignatureArgs = v.object({
8
7
  verifier: v.string(),
9
8
  signature: v.string()
10
9
  });
11
- function verifierSignatureImpl(ctx, args, config) {
10
+ async function verifierSignatureImpl(ctx, args, config) {
12
11
  const { verifier, signature } = args;
13
12
  const db = authDb(ctx, config);
14
13
  const invalidVerifierError = new ConvexError({
15
14
  code: "INVALID_VERIFIER",
16
15
  message: "Invalid or expired verifier."
17
16
  });
18
- return Effect.gen(function* () {
19
- const verifierDoc = yield* Effect.tryPromise({
20
- try: () => db.verifiers.getById(verifier),
21
- catch: () => invalidVerifierError
22
- });
23
- const existingVerifier = yield* pipe(Option.fromNullishOr(verifierDoc), Option.match({
24
- onNone: () => Effect.fail(invalidVerifierError),
25
- onSome: (verifierDoc$1) => Effect.succeed(verifierDoc$1)
26
- }));
27
- yield* Effect.promise(() => db.verifiers.patch(existingVerifier._id, { signature }));
28
- });
17
+ let verifierDoc;
18
+ try {
19
+ verifierDoc = await db.verifiers.getById(verifier);
20
+ } catch {
21
+ throw invalidVerifierError;
22
+ }
23
+ if (verifierDoc == null) throw invalidVerifierError;
24
+ await db.verifiers.patch(verifierDoc._id, { signature });
29
25
  }
30
26
  const callVerifierSignature = async (ctx, args) => {
31
27
  return ctx.runMutation(AUTH_STORE_REF, { args: {
@@ -1,7 +1,7 @@
1
1
  import { LOG_LEVELS } from "../../shared/log.js";
2
2
  import { log } from "../log.js";
3
3
  import { AUTH_STORE_REF } from "./store/refs.js";
4
- import { createNewAndDeleteExistingSession, maybeGenerateTokensForSession } from "../sessions.js";
4
+ import { getAuthSessionId, issueSession } from "../sessions.js";
5
5
  import { v } from "convex/values";
6
6
 
7
7
  //#region src/server/mutations/signin.ts
@@ -13,8 +13,12 @@ const signInArgs = v.object({
13
13
  async function signInImpl(ctx, args, config) {
14
14
  log(LOG_LEVELS.DEBUG, "signInImpl args:", args);
15
15
  const { userId, sessionId: existingSessionId, generateTokens } = args;
16
- const typedUserId = userId;
17
- return await maybeGenerateTokensForSession(ctx, config, typedUserId, existingSessionId ?? await createNewAndDeleteExistingSession(ctx, config, typedUserId), generateTokens);
16
+ return await issueSession(ctx, config, {
17
+ userId,
18
+ existingSessionId,
19
+ replaceSessionId: existingSessionId === void 0 ? await getAuthSessionId(ctx) ?? void 0 : void 0,
20
+ generateTokens
21
+ });
18
22
  }
19
23
  const callSignIn = async (ctx, args) => {
20
24
  return ctx.runMutation(AUTH_STORE_REF, { args: {