@robelest/convex-auth 0.0.2-preview.0 → 0.0.2-preview.2

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 (166) hide show
  1. package/dist/bin.cjs +17 -15
  2. package/dist/client/index.d.ts +84 -30
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +259 -59
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +46 -120
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/index.d.ts +2 -4
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/index.js +2 -4
  11. package/dist/component/index.js.map +1 -1
  12. package/dist/component/public.d.ts +233 -167
  13. package/dist/component/public.d.ts.map +1 -1
  14. package/dist/component/public.js +328 -155
  15. package/dist/component/public.js.map +1 -1
  16. package/dist/component/schema.d.ts +127 -12
  17. package/dist/component/schema.d.ts.map +1 -1
  18. package/dist/component/schema.js +136 -10
  19. package/dist/component/schema.js.map +1 -1
  20. package/dist/providers/{Anonymous.d.ts → anonymous.d.ts} +8 -8
  21. package/dist/providers/{Anonymous.d.ts.map → anonymous.d.ts.map} +1 -1
  22. package/dist/providers/{Anonymous.js → anonymous.js} +9 -10
  23. package/dist/providers/anonymous.js.map +1 -0
  24. package/dist/providers/{ConvexCredentials.d.ts → credentials.d.ts} +11 -11
  25. package/dist/providers/credentials.d.ts.map +1 -0
  26. package/dist/providers/{ConvexCredentials.js → credentials.js} +8 -8
  27. package/dist/providers/credentials.js.map +1 -0
  28. package/dist/providers/{Email.d.ts → email.d.ts} +6 -6
  29. package/dist/providers/email.d.ts.map +1 -0
  30. package/dist/providers/{Email.js → email.js} +6 -6
  31. package/dist/providers/email.js.map +1 -0
  32. package/dist/providers/{Password.d.ts → password.d.ts} +10 -10
  33. package/dist/providers/{Password.d.ts.map → password.d.ts.map} +1 -1
  34. package/dist/providers/{Password.js → password.js} +19 -20
  35. package/dist/providers/password.js.map +1 -0
  36. package/dist/providers/{Phone.d.ts → phone.d.ts} +3 -3
  37. package/dist/providers/{Phone.d.ts.map → phone.d.ts.map} +1 -1
  38. package/dist/providers/{Phone.js → phone.js} +3 -3
  39. package/dist/providers/{Phone.js.map → phone.js.map} +1 -1
  40. package/dist/server/implementation/db.d.ts +5 -2
  41. package/dist/server/implementation/db.d.ts.map +1 -1
  42. package/dist/server/implementation/db.js +2 -1
  43. package/dist/server/implementation/db.js.map +1 -1
  44. package/dist/server/implementation/index.d.ts +285 -180
  45. package/dist/server/implementation/index.d.ts.map +1 -1
  46. package/dist/server/implementation/index.js +280 -173
  47. package/dist/server/implementation/index.js.map +1 -1
  48. package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts.map +1 -1
  49. package/dist/server/implementation/mutations/createAccountFromCredentials.js +8 -18
  50. package/dist/server/implementation/mutations/createAccountFromCredentials.js.map +1 -1
  51. package/dist/server/implementation/mutations/createVerificationCode.d.ts.map +1 -1
  52. package/dist/server/implementation/mutations/createVerificationCode.js +16 -44
  53. package/dist/server/implementation/mutations/createVerificationCode.js.map +1 -1
  54. package/dist/server/implementation/mutations/invalidateSessions.d.ts.map +1 -1
  55. package/dist/server/implementation/mutations/invalidateSessions.js +4 -8
  56. package/dist/server/implementation/mutations/invalidateSessions.js.map +1 -1
  57. package/dist/server/implementation/mutations/modifyAccount.d.ts.map +1 -1
  58. package/dist/server/implementation/mutations/modifyAccount.js +8 -19
  59. package/dist/server/implementation/mutations/modifyAccount.js.map +1 -1
  60. package/dist/server/implementation/mutations/refreshSession.d.ts.map +1 -1
  61. package/dist/server/implementation/mutations/refreshSession.js +9 -23
  62. package/dist/server/implementation/mutations/refreshSession.js.map +1 -1
  63. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.d.ts.map +1 -1
  64. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js +6 -12
  65. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js.map +1 -1
  66. package/dist/server/implementation/mutations/signIn.d.ts.map +1 -1
  67. package/dist/server/implementation/mutations/signIn.js +2 -1
  68. package/dist/server/implementation/mutations/signIn.js.map +1 -1
  69. package/dist/server/implementation/mutations/signOut.d.ts.map +1 -1
  70. package/dist/server/implementation/mutations/signOut.js +5 -6
  71. package/dist/server/implementation/mutations/signOut.js.map +1 -1
  72. package/dist/server/implementation/mutations/storeRef.d.ts +8 -0
  73. package/dist/server/implementation/mutations/storeRef.d.ts.map +1 -0
  74. package/dist/server/implementation/mutations/storeRef.js +8 -0
  75. package/dist/server/implementation/mutations/storeRef.js.map +1 -0
  76. package/dist/server/implementation/mutations/userOAuth.d.ts.map +1 -1
  77. package/dist/server/implementation/mutations/userOAuth.js +16 -53
  78. package/dist/server/implementation/mutations/userOAuth.js.map +1 -1
  79. package/dist/server/implementation/mutations/verifier.d.ts.map +1 -1
  80. package/dist/server/implementation/mutations/verifier.js +4 -8
  81. package/dist/server/implementation/mutations/verifier.js.map +1 -1
  82. package/dist/server/implementation/mutations/verifierSignature.d.ts.map +1 -1
  83. package/dist/server/implementation/mutations/verifierSignature.js +6 -10
  84. package/dist/server/implementation/mutations/verifierSignature.js.map +1 -1
  85. package/dist/server/implementation/mutations/verifyCodeAndSignIn.d.ts.map +1 -1
  86. package/dist/server/implementation/mutations/verifyCodeAndSignIn.js +7 -16
  87. package/dist/server/implementation/mutations/verifyCodeAndSignIn.js.map +1 -1
  88. package/dist/server/implementation/provider.d.ts +2 -1
  89. package/dist/server/implementation/provider.d.ts.map +1 -1
  90. package/dist/server/implementation/provider.js.map +1 -1
  91. package/dist/server/implementation/rateLimit.d.ts.map +1 -1
  92. package/dist/server/implementation/rateLimit.js +13 -39
  93. package/dist/server/implementation/rateLimit.js.map +1 -1
  94. package/dist/server/implementation/refreshTokens.d.ts +1 -8
  95. package/dist/server/implementation/refreshTokens.d.ts.map +1 -1
  96. package/dist/server/implementation/refreshTokens.js +14 -58
  97. package/dist/server/implementation/refreshTokens.js.map +1 -1
  98. package/dist/server/implementation/sessions.d.ts +2 -20
  99. package/dist/server/implementation/sessions.d.ts.map +1 -1
  100. package/dist/server/implementation/sessions.js +8 -35
  101. package/dist/server/implementation/sessions.js.map +1 -1
  102. package/dist/server/implementation/types.d.ts +11 -267
  103. package/dist/server/implementation/types.d.ts.map +1 -1
  104. package/dist/server/implementation/types.js +1 -181
  105. package/dist/server/implementation/types.js.map +1 -1
  106. package/dist/server/implementation/users.d.ts.map +1 -1
  107. package/dist/server/implementation/users.js +19 -67
  108. package/dist/server/implementation/users.js.map +1 -1
  109. package/dist/server/index.d.ts +18 -0
  110. package/dist/server/index.d.ts.map +1 -1
  111. package/dist/server/index.js +255 -0
  112. package/dist/server/index.js.map +1 -1
  113. package/dist/server/provider_utils.d.ts +1 -1
  114. package/dist/server/provider_utils.d.ts.map +1 -1
  115. package/dist/server/provider_utils.js +2 -2
  116. package/dist/server/provider_utils.js.map +1 -1
  117. package/dist/server/types.d.ts +91 -52
  118. package/dist/server/types.d.ts.map +1 -1
  119. package/package.json +3 -6
  120. package/src/cli/index.ts +20 -19
  121. package/src/client/index.ts +347 -110
  122. package/src/component/_generated/component.ts +55 -214
  123. package/src/component/index.ts +1 -11
  124. package/src/component/public.ts +366 -178
  125. package/src/component/schema.ts +150 -19
  126. package/src/providers/{Anonymous.ts → anonymous.ts} +10 -11
  127. package/src/providers/{ConvexCredentials.ts → credentials.ts} +11 -11
  128. package/src/providers/{Email.ts → email.ts} +5 -5
  129. package/src/providers/{Password.ts → password.ts} +22 -27
  130. package/src/providers/{Phone.ts → phone.ts} +2 -2
  131. package/src/server/implementation/db.ts +5 -2
  132. package/src/server/implementation/index.ts +368 -313
  133. package/src/server/implementation/mutations/createAccountFromCredentials.ts +11 -25
  134. package/src/server/implementation/mutations/createVerificationCode.ts +16 -47
  135. package/src/server/implementation/mutations/invalidateSessions.ts +4 -9
  136. package/src/server/implementation/mutations/modifyAccount.ts +8 -22
  137. package/src/server/implementation/mutations/refreshSession.ts +11 -24
  138. package/src/server/implementation/mutations/retrieveAccountWithCredentials.ts +9 -17
  139. package/src/server/implementation/mutations/signIn.ts +2 -1
  140. package/src/server/implementation/mutations/signOut.ts +5 -8
  141. package/src/server/implementation/mutations/storeRef.ts +7 -0
  142. package/src/server/implementation/mutations/userOAuth.ts +10 -50
  143. package/src/server/implementation/mutations/verifier.ts +4 -9
  144. package/src/server/implementation/mutations/verifierSignature.ts +6 -12
  145. package/src/server/implementation/mutations/verifyCodeAndSignIn.ts +7 -18
  146. package/src/server/implementation/provider.ts +2 -1
  147. package/src/server/implementation/rateLimit.ts +15 -41
  148. package/src/server/implementation/refreshTokens.ts +26 -76
  149. package/src/server/implementation/sessions.ts +8 -39
  150. package/src/server/implementation/types.ts +16 -191
  151. package/src/server/implementation/users.ts +19 -66
  152. package/src/server/index.ts +373 -0
  153. package/src/server/provider_utils.ts +2 -2
  154. package/src/server/types.ts +116 -51
  155. package/dist/providers/Anonymous.js.map +0 -1
  156. package/dist/providers/ConvexCredentials.d.ts.map +0 -1
  157. package/dist/providers/ConvexCredentials.js.map +0 -1
  158. package/dist/providers/Email.d.ts.map +0 -1
  159. package/dist/providers/Email.js.map +0 -1
  160. package/dist/providers/Password.js.map +0 -1
  161. package/providers/Anonymous/package.json +0 -6
  162. package/providers/ConvexCredentials/package.json +0 -6
  163. package/providers/Email/package.json +0 -6
  164. package/providers/Password/package.json +0 -6
  165. package/providers/Phone/package.json +0 -6
  166. package/server/package.json +0 -6
@@ -1,12 +1,20 @@
1
- import { v } from "convex/values";
1
+ import { ConvexError, v } from "convex/values";
2
2
  import { mutation, query } from "./_generated/server";
3
+ // ============================================================================
3
4
  // Users
5
+ // ============================================================================
6
+ /** Retrieve a user by their document ID. */
4
7
  export const userGetById = query({
5
8
  args: { userId: v.id("user") },
6
9
  handler: async (ctx, { userId }) => {
7
10
  return await ctx.db.get(userId);
8
11
  },
9
12
  });
13
+ /**
14
+ * Find a user by their verified email address. Returns `null` if no user
15
+ * has this email verified, or if multiple users share the same verified email
16
+ * (ambiguous — should not happen in normal operation).
17
+ */
10
18
  export const userFindByVerifiedEmail = query({
11
19
  args: { email: v.string() },
12
20
  handler: async (ctx, { email }) => {
@@ -18,6 +26,11 @@ export const userFindByVerifiedEmail = query({
18
26
  return users.length === 1 ? users[0] : null;
19
27
  },
20
28
  });
29
+ /**
30
+ * Find a user by their verified phone number. Returns `null` if no user
31
+ * has this phone verified, or if multiple users share the same verified phone
32
+ * (ambiguous — should not happen in normal operation).
33
+ */
21
34
  export const userFindByVerifiedPhone = query({
22
35
  args: { phone: v.string() },
23
36
  handler: async (ctx, { phone }) => {
@@ -29,12 +42,14 @@ export const userFindByVerifiedPhone = query({
29
42
  return users.length === 1 ? users[0] : null;
30
43
  },
31
44
  });
45
+ /** Insert a new user document. */
32
46
  export const userInsert = mutation({
33
47
  args: { data: v.any() },
34
48
  handler: async (ctx, { data }) => {
35
49
  return await ctx.db.insert("user", data);
36
50
  },
37
51
  });
52
+ /** Insert a new user or update an existing one. */
38
53
  export const userUpsert = mutation({
39
54
  args: { userId: v.optional(v.id("user")), data: v.any() },
40
55
  handler: async (ctx, { userId, data }) => {
@@ -45,13 +60,17 @@ export const userUpsert = mutation({
45
60
  return await ctx.db.insert("user", data);
46
61
  },
47
62
  });
63
+ /** Patch an existing user document with partial data. */
48
64
  export const userPatch = mutation({
49
65
  args: { userId: v.id("user"), data: v.any() },
50
66
  handler: async (ctx, { userId, data }) => {
51
67
  await ctx.db.patch(userId, data);
52
68
  },
53
69
  });
70
+ // ============================================================================
54
71
  // Accounts
72
+ // ============================================================================
73
+ /** Look up an account by provider and provider-specific account ID. */
55
74
  export const accountGet = query({
56
75
  args: { provider: v.string(), providerAccountId: v.string() },
57
76
  handler: async (ctx, { provider, providerAccountId }) => {
@@ -61,12 +80,14 @@ export const accountGet = query({
61
80
  .unique();
62
81
  },
63
82
  });
83
+ /** Retrieve an account by its document ID. */
64
84
  export const accountGetById = query({
65
85
  args: { accountId: v.id("account") },
66
86
  handler: async (ctx, { accountId }) => {
67
87
  return await ctx.db.get(accountId);
68
88
  },
69
89
  });
90
+ /** Create a new account linking a user to an auth provider. */
70
91
  export const accountInsert = mutation({
71
92
  args: {
72
93
  userId: v.id("user"),
@@ -78,19 +99,24 @@ export const accountInsert = mutation({
78
99
  return await ctx.db.insert("account", args);
79
100
  },
80
101
  });
102
+ /** Patch an existing account document with partial data. */
81
103
  export const accountPatch = mutation({
82
104
  args: { accountId: v.id("account"), data: v.any() },
83
105
  handler: async (ctx, { accountId, data }) => {
84
106
  await ctx.db.patch(accountId, data);
85
107
  },
86
108
  });
109
+ /** Delete an account document. */
87
110
  export const accountDelete = mutation({
88
111
  args: { accountId: v.id("account") },
89
112
  handler: async (ctx, { accountId }) => {
90
113
  await ctx.db.delete(accountId);
91
114
  },
92
115
  });
116
+ // ============================================================================
93
117
  // Sessions
118
+ // ============================================================================
119
+ /** Create a new session for a user with an expiration time. */
94
120
  export const sessionCreate = mutation({
95
121
  args: { userId: v.id("user"), expirationTime: v.number() },
96
122
  handler: async (ctx, { userId, expirationTime }) => {
@@ -100,12 +126,14 @@ export const sessionCreate = mutation({
100
126
  });
101
127
  },
102
128
  });
129
+ /** Retrieve a session by its document ID. */
103
130
  export const sessionGetById = query({
104
131
  args: { sessionId: v.id("session") },
105
132
  handler: async (ctx, { sessionId }) => {
106
133
  return await ctx.db.get(sessionId);
107
134
  },
108
135
  });
136
+ /** Delete a session. No-op if the session does not exist. */
109
137
  export const sessionDelete = mutation({
110
138
  args: { sessionId: v.id("session") },
111
139
  handler: async (ctx, { sessionId }) => {
@@ -114,6 +142,7 @@ export const sessionDelete = mutation({
114
142
  }
115
143
  },
116
144
  });
145
+ /** List all sessions for a user. */
117
146
  export const sessionListByUser = query({
118
147
  args: { userId: v.id("user") },
119
148
  handler: async (ctx, { userId }) => {
@@ -123,19 +152,24 @@ export const sessionListByUser = query({
123
152
  .collect();
124
153
  },
125
154
  });
155
+ // ============================================================================
126
156
  // Verifiers
157
+ // ============================================================================
158
+ /** Create a new PKCE verifier, optionally linked to a session. */
127
159
  export const verifierCreate = mutation({
128
160
  args: { sessionId: v.optional(v.id("session")) },
129
161
  handler: async (ctx, { sessionId }) => {
130
162
  return await ctx.db.insert("verifier", { sessionId: sessionId });
131
163
  },
132
164
  });
165
+ /** Retrieve a verifier by its document ID. */
133
166
  export const verifierGetById = query({
134
167
  args: { verifierId: v.id("verifier") },
135
168
  handler: async (ctx, { verifierId }) => {
136
169
  return await ctx.db.get(verifierId);
137
170
  },
138
171
  });
172
+ /** Look up a verifier by its cryptographic signature. */
139
173
  export const verifierGetBySignature = query({
140
174
  args: { signature: v.string() },
141
175
  handler: async (ctx, { signature }) => {
@@ -145,19 +179,24 @@ export const verifierGetBySignature = query({
145
179
  .unique();
146
180
  },
147
181
  });
182
+ /** Patch a verifier document with partial data. */
148
183
  export const verifierPatch = mutation({
149
184
  args: { verifierId: v.id("verifier"), data: v.any() },
150
185
  handler: async (ctx, { verifierId, data }) => {
151
186
  await ctx.db.patch(verifierId, data);
152
187
  },
153
188
  });
189
+ /** Delete a verifier document. */
154
190
  export const verifierDelete = mutation({
155
191
  args: { verifierId: v.id("verifier") },
156
192
  handler: async (ctx, { verifierId }) => {
157
193
  await ctx.db.delete(verifierId);
158
194
  },
159
195
  });
160
- // Verification codes
196
+ // ============================================================================
197
+ // Verification Codes
198
+ // ============================================================================
199
+ /** Find a verification code by its associated account ID. */
161
200
  export const verificationCodeGetByAccountId = query({
162
201
  args: { accountId: v.id("account") },
163
202
  handler: async (ctx, { accountId }) => {
@@ -167,6 +206,7 @@ export const verificationCodeGetByAccountId = query({
167
206
  .unique();
168
207
  },
169
208
  });
209
+ /** Find a verification code by its code string. */
170
210
  export const verificationCodeGetByCode = query({
171
211
  args: { code: v.string() },
172
212
  handler: async (ctx, { code }) => {
@@ -176,6 +216,7 @@ export const verificationCodeGetByCode = query({
176
216
  .unique();
177
217
  },
178
218
  });
219
+ /** Create a new verification code for OTP, magic link, or OAuth flows. */
179
220
  export const verificationCodeCreate = mutation({
180
221
  args: {
181
222
  accountId: v.id("account"),
@@ -190,13 +231,17 @@ export const verificationCodeCreate = mutation({
190
231
  return await ctx.db.insert("verification", args);
191
232
  },
192
233
  });
234
+ /** Delete a verification code document. */
193
235
  export const verificationCodeDelete = mutation({
194
236
  args: { verificationCodeId: v.id("verification") },
195
237
  handler: async (ctx, { verificationCodeId }) => {
196
238
  await ctx.db.delete(verificationCodeId);
197
239
  },
198
240
  });
199
- // Refresh tokens
241
+ // ============================================================================
242
+ // Refresh Tokens
243
+ // ============================================================================
244
+ /** Create a new refresh token for a session. */
200
245
  export const refreshTokenCreate = mutation({
201
246
  args: {
202
247
  sessionId: v.id("session"),
@@ -207,18 +252,21 @@ export const refreshTokenCreate = mutation({
207
252
  return await ctx.db.insert("token", args);
208
253
  },
209
254
  });
255
+ /** Retrieve a refresh token by its document ID. */
210
256
  export const refreshTokenGetById = query({
211
257
  args: { refreshTokenId: v.id("token") },
212
258
  handler: async (ctx, { refreshTokenId }) => {
213
259
  return await ctx.db.get(refreshTokenId);
214
260
  },
215
261
  });
262
+ /** Patch a refresh token document with partial data. */
216
263
  export const refreshTokenPatch = mutation({
217
264
  args: { refreshTokenId: v.id("token"), data: v.any() },
218
265
  handler: async (ctx, { refreshTokenId, data }) => {
219
266
  await ctx.db.patch(refreshTokenId, data);
220
267
  },
221
268
  });
269
+ /** Get child tokens that were created by exchanging a specific parent token. */
222
270
  export const refreshTokenGetChildren = query({
223
271
  args: {
224
272
  sessionId: v.id("session"),
@@ -233,6 +281,7 @@ export const refreshTokenGetChildren = query({
233
281
  .collect();
234
282
  },
235
283
  });
284
+ /** List all refresh tokens for a session. */
236
285
  export const refreshTokenListBySession = query({
237
286
  args: { sessionId: v.id("session") },
238
287
  handler: async (ctx, { sessionId }) => {
@@ -242,6 +291,7 @@ export const refreshTokenListBySession = query({
242
291
  .collect();
243
292
  },
244
293
  });
294
+ /** Delete all refresh tokens for a session. */
245
295
  export const refreshTokenDeleteAll = mutation({
246
296
  args: { sessionId: v.id("session") },
247
297
  handler: async (ctx, { sessionId }) => {
@@ -252,6 +302,7 @@ export const refreshTokenDeleteAll = mutation({
252
302
  await Promise.all(tokens.map((token) => ctx.db.delete(token._id)));
253
303
  },
254
304
  });
305
+ /** Get the active (unused) refresh token for a session. */
255
306
  export const refreshTokenGetActive = query({
256
307
  args: { sessionId: v.id("session") },
257
308
  handler: async (ctx, { sessionId }) => {
@@ -263,7 +314,10 @@ export const refreshTokenGetActive = query({
263
314
  .first();
264
315
  },
265
316
  });
266
- // Rate limits
317
+ // ============================================================================
318
+ // Rate Limits
319
+ // ============================================================================
320
+ /** Look up a rate limit entry by its identifier. */
267
321
  export const rateLimitGet = query({
268
322
  args: { identifier: v.string() },
269
323
  handler: async (ctx, { identifier }) => {
@@ -273,6 +327,7 @@ export const rateLimitGet = query({
273
327
  .unique();
274
328
  },
275
329
  });
330
+ /** Create a new rate limit entry. */
276
331
  export const rateLimitCreate = mutation({
277
332
  args: {
278
333
  identifier: v.string(),
@@ -283,245 +338,363 @@ export const rateLimitCreate = mutation({
283
338
  return await ctx.db.insert("limit", args);
284
339
  },
285
340
  });
341
+ /** Patch a rate limit entry with partial data. */
286
342
  export const rateLimitPatch = mutation({
287
343
  args: { rateLimitId: v.id("limit"), data: v.any() },
288
344
  handler: async (ctx, { rateLimitId, data }) => {
289
345
  await ctx.db.patch(rateLimitId, data);
290
346
  },
291
347
  });
348
+ /** Delete a rate limit entry. */
292
349
  export const rateLimitDelete = mutation({
293
350
  args: { rateLimitId: v.id("limit") },
294
351
  handler: async (ctx, { rateLimitId }) => {
295
352
  await ctx.db.delete(rateLimitId);
296
353
  },
297
354
  });
298
- // Singular aliases
299
- export const verificationGetByAccountId = verificationCodeGetByAccountId;
300
- export const verificationGetByCode = verificationCodeGetByCode;
301
- export const verificationCreate = verificationCodeCreate;
302
- export const verificationDelete = verificationCodeDelete;
303
- export const tokenCreate = refreshTokenCreate;
304
- export const tokenGetById = refreshTokenGetById;
305
- export const tokenPatch = refreshTokenPatch;
306
- export const tokenGetChildren = refreshTokenGetChildren;
307
- export const tokenListBySession = refreshTokenListBySession;
308
- export const tokenDeleteAll = refreshTokenDeleteAll;
309
- export const tokenGetActive = refreshTokenGetActive;
310
- export const limitGet = rateLimitGet;
311
- export const limitCreate = rateLimitCreate;
312
- export const limitPatch = rateLimitPatch;
313
- export const limitDelete = rateLimitDelete;
314
- // Organization
315
- export const organizationCreate = mutation({
316
- args: { data: v.any() },
317
- handler: async (ctx, { data }) => {
318
- return await ctx.db.insert("organization", data);
319
- },
320
- });
321
- export const organizationGet = query({
322
- args: { organizationId: v.id("organization") },
323
- handler: async (ctx, { organizationId }) => {
324
- return await ctx.db.get(organizationId);
325
- },
326
- });
327
- export const organizationList = query({
328
- args: { ownerUserId: v.optional(v.id("user")) },
329
- handler: async (ctx, { ownerUserId }) => {
330
- if (ownerUserId === undefined) {
331
- return await ctx.db.query("organization").collect();
332
- }
333
- return await ctx.db
334
- .query("organization")
335
- .withIndex("ownerUserId", (q) => q.eq("ownerUserId", ownerUserId))
336
- .collect();
337
- },
338
- });
339
- export const organizationUpdate = mutation({
340
- args: { organizationId: v.id("organization"), data: v.any() },
341
- handler: async (ctx, { organizationId, data }) => {
342
- await ctx.db.patch(organizationId, data);
343
- },
344
- });
345
- export const organizationDelete = mutation({
346
- args: { organizationId: v.id("organization") },
347
- handler: async (ctx, { organizationId }) => {
348
- await ctx.db.delete(organizationId);
349
- },
350
- });
351
- // Team
352
- export const teamCreate = mutation({
355
+ // ============================================================================
356
+ // Groups
357
+ // ============================================================================
358
+ /**
359
+ * Create a new group. Groups are hierarchical — set `parentGroupId` to nest
360
+ * under an existing group, or omit it to create a root-level group.
361
+ *
362
+ * @returns The ID of the newly created group.
363
+ */
364
+ export const groupCreate = mutation({
353
365
  args: {
354
- organizationId: v.id("organization"),
355
366
  name: v.string(),
356
367
  slug: v.optional(v.string()),
357
- parentTeamId: v.optional(v.id("team")),
358
- metadata: v.optional(v.any()),
368
+ parentGroupId: v.optional(v.id("group")),
369
+ extend: v.optional(v.any()),
359
370
  },
360
371
  handler: async (ctx, args) => {
361
- return await ctx.db.insert("team", args);
372
+ return await ctx.db.insert("group", args);
362
373
  },
363
374
  });
364
- export const teamGet = query({
365
- args: { teamId: v.id("team") },
366
- handler: async (ctx, { teamId }) => {
367
- return await ctx.db.get(teamId);
375
+ /** Retrieve a group by its document ID. Returns `null` if not found. */
376
+ export const groupGet = query({
377
+ args: { groupId: v.id("group") },
378
+ handler: async (ctx, { groupId }) => {
379
+ return await ctx.db.get(groupId);
368
380
  },
369
381
  });
370
- export const teamListByOrganization = query({
371
- args: { organizationId: v.id("organization") },
372
- handler: async (ctx, { organizationId }) => {
382
+ /**
383
+ * List groups. When `parentGroupId` is provided, returns children of that
384
+ * group. When omitted, returns all root-level groups (groups with no parent).
385
+ */
386
+ export const groupList = query({
387
+ args: { parentGroupId: v.optional(v.id("group")) },
388
+ handler: async (ctx, { parentGroupId }) => {
373
389
  return await ctx.db
374
- .query("team")
375
- .withIndex("organizationId", (q) => q.eq("organizationId", organizationId))
390
+ .query("group")
391
+ .withIndex("parentGroupId", (q) => q.eq("parentGroupId", parentGroupId))
376
392
  .collect();
377
393
  },
378
394
  });
379
- export const teamUpdate = mutation({
380
- args: { teamId: v.id("team"), data: v.any() },
381
- handler: async (ctx, { teamId, data }) => {
382
- await ctx.db.patch(teamId, data);
383
- },
384
- });
385
- export const teamDelete = mutation({
386
- args: { teamId: v.id("team") },
387
- handler: async (ctx, { teamId }) => {
388
- await ctx.db.delete(teamId);
395
+ /** Update a group's fields (name, slug, extend, parentGroupId). */
396
+ export const groupUpdate = mutation({
397
+ args: { groupId: v.id("group"), data: v.any() },
398
+ handler: async (ctx, { groupId, data }) => {
399
+ await ctx.db.patch(groupId, data);
400
+ },
401
+ });
402
+ /**
403
+ * Delete a group and all of its descendants. This cascades to:
404
+ * - All child groups (recursively)
405
+ * - All members of this group and its descendants
406
+ * - All invites for this group and its descendants
407
+ */
408
+ export const groupDelete = mutation({
409
+ args: { groupId: v.id("group") },
410
+ handler: async (ctx, { groupId }) => {
411
+ const deleteGroup = async (id) => {
412
+ const children = await ctx.db
413
+ .query("group")
414
+ .withIndex("parentGroupId", (q) => q.eq("parentGroupId", id))
415
+ .collect();
416
+ for (const child of children) {
417
+ await deleteGroup(child._id);
418
+ }
419
+ const members = await ctx.db
420
+ .query("member")
421
+ .withIndex("groupId", (q) => q.eq("groupId", id))
422
+ .collect();
423
+ for (const member of members) {
424
+ await ctx.db.delete(member._id);
425
+ }
426
+ const invites = await ctx.db
427
+ .query("invite")
428
+ .withIndex("groupId", (q) => q.eq("groupId", id))
429
+ .collect();
430
+ for (const invite of invites) {
431
+ await ctx.db.delete(invite._id);
432
+ }
433
+ await ctx.db.delete(id);
434
+ };
435
+ await deleteGroup(groupId);
389
436
  },
390
437
  });
391
- // Team relations
392
- export const teamRelationCreate = mutation({
438
+ // ============================================================================
439
+ // Members
440
+ // ============================================================================
441
+ /**
442
+ * Add a user as a member of a group.
443
+ *
444
+ * The `role` field is an application-defined string (e.g. "owner", "admin",
445
+ * "member", "viewer"). The auth component stores it but does not enforce
446
+ * access control — your application defines what each role means.
447
+ *
448
+ * Throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` when the user is
449
+ * already a member of the target group.
450
+ *
451
+ * @returns The ID of the new member record.
452
+ */
453
+ export const memberAdd = mutation({
393
454
  args: {
394
- organizationId: v.id("organization"),
395
- parentTeamId: v.id("team"),
396
- childTeamId: v.id("team"),
397
- relation: v.optional(v.string()),
455
+ groupId: v.id("group"),
456
+ userId: v.id("user"),
457
+ role: v.optional(v.string()),
458
+ status: v.optional(v.string()),
459
+ extend: v.optional(v.any()),
398
460
  },
399
461
  handler: async (ctx, args) => {
400
- return await ctx.db.insert("teamRelation", args);
462
+ const existingMembership = await ctx.db
463
+ .query("member")
464
+ .withIndex("groupIdAndUserId", (q) => q.eq("groupId", args.groupId).eq("userId", args.userId))
465
+ .unique();
466
+ if (existingMembership !== null) {
467
+ throw new ConvexError({
468
+ code: "DUPLICATE_MEMBERSHIP",
469
+ message: "User is already a member of this group",
470
+ groupId: args.groupId,
471
+ userId: args.userId,
472
+ existingMemberId: existingMembership._id,
473
+ });
474
+ }
475
+ return await ctx.db.insert("member", args);
401
476
  },
402
477
  });
403
- export const teamRelationGet = query({
404
- args: { teamRelationId: v.id("teamRelation") },
405
- handler: async (ctx, { teamRelationId }) => {
406
- return await ctx.db.get(teamRelationId);
478
+ /** Retrieve a member record by its document ID. Returns `null` if not found. */
479
+ export const memberGet = query({
480
+ args: { memberId: v.id("member") },
481
+ handler: async (ctx, { memberId }) => {
482
+ return await ctx.db.get(memberId);
407
483
  },
408
484
  });
409
- export const teamRelationListByParent = query({
410
- args: {
411
- organizationId: v.id("organization"),
412
- parentTeamId: v.id("team"),
413
- },
414
- handler: async (ctx, { organizationId, parentTeamId }) => {
485
+ /** List all members of a specific group. */
486
+ export const memberList = query({
487
+ args: { groupId: v.id("group") },
488
+ handler: async (ctx, { groupId }) => {
415
489
  return await ctx.db
416
- .query("teamRelation")
417
- .withIndex("organizationIdAndParentTeamId", (q) => q.eq("organizationId", organizationId).eq("parentTeamId", parentTeamId))
490
+ .query("member")
491
+ .withIndex("groupId", (q) => q.eq("groupId", groupId))
418
492
  .collect();
419
493
  },
420
494
  });
421
- export const teamRelationDelete = mutation({
422
- args: { teamRelationId: v.id("teamRelation") },
423
- handler: async (ctx, { teamRelationId }) => {
424
- await ctx.db.delete(teamRelationId);
495
+ /**
496
+ * List all group memberships for a specific user. Returns member records
497
+ * which include the `groupId`, `role`, `status`, and `extend` for each
498
+ * group the user belongs to.
499
+ */
500
+ export const memberListByUser = query({
501
+ args: { userId: v.id("user") },
502
+ handler: async (ctx, { userId }) => {
503
+ return await ctx.db
504
+ .query("member")
505
+ .withIndex("userId", (q) => q.eq("userId", userId))
506
+ .collect();
425
507
  },
426
508
  });
427
- // Members
428
- export const memberAdd = mutation({
429
- args: { data: v.any() },
430
- handler: async (ctx, { data }) => {
431
- return await ctx.db.insert("member", data);
509
+ /**
510
+ * Look up a specific user's membership in a specific group.
511
+ * Returns `null` if the user is not a member of the group.
512
+ */
513
+ export const memberGetByGroupAndUser = query({
514
+ args: { groupId: v.id("group"), userId: v.id("user") },
515
+ handler: async (ctx, { groupId, userId }) => {
516
+ return await ctx.db
517
+ .query("member")
518
+ .withIndex("groupIdAndUserId", (q) => q.eq("groupId", groupId).eq("userId", userId))
519
+ .unique();
432
520
  },
433
521
  });
522
+ /** Remove a member from a group by deleting the member record. */
434
523
  export const memberRemove = mutation({
435
524
  args: { memberId: v.id("member") },
436
525
  handler: async (ctx, { memberId }) => {
437
526
  await ctx.db.delete(memberId);
438
527
  },
439
528
  });
440
- export const memberList = query({
441
- args: {
442
- organizationId: v.id("organization"),
443
- teamId: v.optional(v.id("team")),
444
- },
445
- handler: async (ctx, { organizationId, teamId }) => {
446
- if (teamId !== undefined) {
447
- return await ctx.db
448
- .query("member")
449
- .withIndex("teamId", (q) => q.eq("teamId", teamId))
450
- .collect();
451
- }
452
- return await ctx.db
453
- .query("member")
454
- .withIndex("organizationId", (q) => q.eq("organizationId", organizationId))
455
- .collect();
456
- },
457
- });
458
- export const memberRoleSet = mutation({
459
- args: { memberId: v.id("member"), role: v.string() },
460
- handler: async (ctx, { memberId, role }) => {
461
- await ctx.db.patch(memberId, { role });
462
- },
463
- });
464
- export const memberRoleGet = query({
465
- args: { memberId: v.id("member") },
466
- handler: async (ctx, { memberId }) => {
467
- const member = await ctx.db.get(memberId);
468
- return member?.role ?? null;
529
+ /**
530
+ * Update a member record's fields (role, status, extend).
531
+ *
532
+ * Common usage: `memberUpdate({ memberId, data: { role: "admin" } })`
533
+ */
534
+ export const memberUpdate = mutation({
535
+ args: { memberId: v.id("member"), data: v.any() },
536
+ handler: async (ctx, { memberId, data }) => {
537
+ await ctx.db.patch(memberId, data);
469
538
  },
470
539
  });
540
+ // ============================================================================
471
541
  // Invites
542
+ // ============================================================================
543
+ /**
544
+ * Create a new platform-level invitation. Optionally set `groupId` to tie
545
+ * the invite to a specific group. The invitation is sent to an email address
546
+ * and includes a hashed token for secure acceptance.
547
+ *
548
+ * Throws `ConvexError` with code `DUPLICATE_INVITE` when a pending invite
549
+ * already exists for the same email and scope:
550
+ * - group invite: same `email` + same `groupId`
551
+ * - platform invite: same `email` with no `groupId`
552
+ *
553
+ * @returns The ID of the new invite record.
554
+ */
472
555
  export const inviteCreate = mutation({
473
- args: { data: v.any() },
474
- handler: async (ctx, { data }) => {
475
- return await ctx.db.insert("invite", data);
556
+ args: {
557
+ groupId: v.optional(v.id("group")),
558
+ invitedByUserId: v.id("user"),
559
+ email: v.string(),
560
+ tokenHash: v.string(),
561
+ role: v.optional(v.string()),
562
+ status: v.union(v.literal("pending"), v.literal("accepted"), v.literal("revoked"), v.literal("expired")),
563
+ expiresTime: v.number(),
564
+ extend: v.optional(v.any()),
565
+ },
566
+ handler: async (ctx, args) => {
567
+ if (args.groupId !== undefined) {
568
+ const existingGroupInvite = await ctx.db
569
+ .query("invite")
570
+ .withIndex("groupIdAndStatus", (q) => q.eq("groupId", args.groupId).eq("status", "pending"))
571
+ .filter((q) => q.eq(q.field("email"), args.email))
572
+ .first();
573
+ if (existingGroupInvite !== null) {
574
+ throw new ConvexError({
575
+ code: "DUPLICATE_INVITE",
576
+ message: "A pending invite already exists for this email in this group",
577
+ email: args.email,
578
+ groupId: args.groupId,
579
+ existingInviteId: existingGroupInvite._id,
580
+ });
581
+ }
582
+ }
583
+ else {
584
+ const existingPlatformInvite = await ctx.db
585
+ .query("invite")
586
+ .withIndex("emailAndStatus", (q) => q.eq("email", args.email).eq("status", "pending"))
587
+ .filter((q) => q.eq(q.field("groupId"), undefined))
588
+ .first();
589
+ if (existingPlatformInvite !== null) {
590
+ throw new ConvexError({
591
+ code: "DUPLICATE_INVITE",
592
+ message: "A pending platform invite already exists for this email",
593
+ email: args.email,
594
+ existingInviteId: existingPlatformInvite._id,
595
+ });
596
+ }
597
+ }
598
+ return await ctx.db.insert("invite", args);
476
599
  },
477
600
  });
601
+ /** Retrieve an invite by its document ID. Returns `null` if not found. */
478
602
  export const inviteGet = query({
479
603
  args: { inviteId: v.id("invite") },
480
604
  handler: async (ctx, { inviteId }) => {
481
605
  return await ctx.db.get(inviteId);
482
606
  },
483
607
  });
608
+ /**
609
+ * List invites, optionally filtered by group and/or status.
610
+ * Both `groupId` and `status` are optional filters.
611
+ */
484
612
  export const inviteList = query({
485
613
  args: {
486
- organizationId: v.optional(v.id("organization")),
487
- status: v.optional(v.string()),
614
+ groupId: v.optional(v.id("group")),
615
+ status: v.optional(v.union(v.literal("pending"), v.literal("accepted"), v.literal("revoked"), v.literal("expired"))),
488
616
  },
489
- handler: async (ctx, { organizationId, status }) => {
490
- if (organizationId !== undefined && status !== undefined) {
617
+ handler: async (ctx, { groupId, status }) => {
618
+ if (groupId !== undefined && status !== undefined) {
491
619
  return await ctx.db
492
620
  .query("invite")
493
- .withIndex("organizationIdAndStatus", (q) => q.eq("organizationId", organizationId).eq("status", status))
621
+ .withIndex("groupIdAndStatus", (q) => q.eq("groupId", groupId).eq("status", status))
494
622
  .collect();
495
623
  }
496
- if (organizationId !== undefined) {
624
+ if (groupId !== undefined) {
497
625
  return await ctx.db
498
626
  .query("invite")
499
- .withIndex("organizationId", (q) => q.eq("organizationId", organizationId))
627
+ .withIndex("groupId", (q) => q.eq("groupId", groupId))
500
628
  .collect();
501
629
  }
502
630
  if (status !== undefined) {
503
631
  return await ctx.db
504
632
  .query("invite")
505
- .filter((q) => q.eq(q.field("status"), status))
633
+ .withIndex("status", (q) => q.eq("status", status))
506
634
  .collect();
507
635
  }
508
- return await ctx.db
509
- .query("invite")
510
- .collect();
511
- },
512
- });
636
+ return await ctx.db.query("invite").collect();
637
+ },
638
+ });
639
+ /**
640
+ * Accept a pending invitation.
641
+ *
642
+ * Marks the invite as "accepted" and records the acceptance timestamp.
643
+ * Throws a structured `ConvexError` when the invite doesn't exist or is not
644
+ * currently pending.
645
+ *
646
+ * The caller is responsible for creating the corresponding member record.
647
+ */
513
648
  export const inviteAccept = mutation({
514
649
  args: { inviteId: v.id("invite") },
515
650
  handler: async (ctx, { inviteId }) => {
651
+ const invite = await ctx.db.get(inviteId);
652
+ if (invite === null) {
653
+ throw new ConvexError({
654
+ code: "INVITE_NOT_FOUND",
655
+ message: "Invite not found",
656
+ inviteId,
657
+ });
658
+ }
659
+ if (invite.status !== "pending") {
660
+ throw new ConvexError({
661
+ code: "INVITE_NOT_PENDING",
662
+ message: `Cannot accept invite with status "${invite.status}"`,
663
+ inviteId,
664
+ currentStatus: invite.status,
665
+ });
666
+ }
516
667
  await ctx.db.patch(inviteId, {
517
668
  status: "accepted",
518
669
  acceptedTime: Date.now(),
519
670
  });
520
671
  },
521
672
  });
673
+ /**
674
+ * Revoke a pending invitation.
675
+ *
676
+ * Marks the invite as "revoked". Throws a structured `ConvexError` when the
677
+ * invite doesn't exist or is not currently pending.
678
+ */
522
679
  export const inviteRevoke = mutation({
523
680
  args: { inviteId: v.id("invite") },
524
681
  handler: async (ctx, { inviteId }) => {
682
+ const invite = await ctx.db.get(inviteId);
683
+ if (invite === null) {
684
+ throw new ConvexError({
685
+ code: "INVITE_NOT_FOUND",
686
+ message: "Invite not found",
687
+ inviteId,
688
+ });
689
+ }
690
+ if (invite.status !== "pending") {
691
+ throw new ConvexError({
692
+ code: "INVITE_NOT_PENDING",
693
+ message: `Cannot revoke invite with status "${invite.status}"`,
694
+ inviteId,
695
+ currentStatus: invite.status,
696
+ });
697
+ }
525
698
  await ctx.db.patch(inviteId, { status: "revoked" });
526
699
  },
527
700
  });