@robelest/convex-auth 0.0.4-preview.13 → 0.0.4-preview.15

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 (323) hide show
  1. package/README.md +140 -9
  2. package/dist/bin.cjs +5957 -5478
  3. package/dist/client/index.d.ts +3 -7
  4. package/dist/client/index.d.ts.map +1 -1
  5. package/dist/client/index.js +27 -26
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/component/_generated/api.d.ts +14 -0
  8. package/dist/component/_generated/api.d.ts.map +1 -1
  9. package/dist/component/_generated/api.js.map +1 -1
  10. package/dist/component/_generated/component.d.ts +1513 -3
  11. package/dist/component/_generated/component.d.ts.map +1 -1
  12. package/dist/component/convex.config.d.ts +2 -2
  13. package/dist/component/convex.config.d.ts.map +1 -1
  14. package/dist/component/model.d.ts +153 -0
  15. package/dist/component/model.d.ts.map +1 -0
  16. package/dist/component/model.js +327 -0
  17. package/dist/component/model.js.map +1 -0
  18. package/dist/component/providers/sso.d.ts +1 -1
  19. package/dist/component/public/enterprise.d.ts +49 -0
  20. package/dist/component/public/enterprise.d.ts.map +1 -0
  21. package/dist/component/public/enterprise.js +450 -0
  22. package/dist/component/public/enterprise.js.map +1 -0
  23. package/dist/component/public/factors.d.ts +52 -0
  24. package/dist/component/public/factors.d.ts.map +1 -0
  25. package/dist/component/public/factors.js +285 -0
  26. package/dist/component/public/factors.js.map +1 -0
  27. package/dist/component/public/groups.d.ts +118 -0
  28. package/dist/component/public/groups.d.ts.map +1 -0
  29. package/dist/component/public/groups.js +599 -0
  30. package/dist/component/public/groups.js.map +1 -0
  31. package/dist/component/public/identity.d.ts +93 -0
  32. package/dist/component/public/identity.d.ts.map +1 -0
  33. package/dist/component/public/identity.js +426 -0
  34. package/dist/component/public/identity.js.map +1 -0
  35. package/dist/component/public/keys.d.ts +41 -0
  36. package/dist/component/public/keys.d.ts.map +1 -0
  37. package/dist/component/public/keys.js +157 -0
  38. package/dist/component/public/keys.js.map +1 -0
  39. package/dist/component/public/shared.d.ts +26 -0
  40. package/dist/component/public/shared.d.ts.map +1 -0
  41. package/dist/component/public/shared.js +32 -0
  42. package/dist/component/public/shared.js.map +1 -0
  43. package/dist/component/public.d.ts +9 -321
  44. package/dist/component/public.d.ts.map +1 -1
  45. package/dist/component/public.js +6 -2145
  46. package/dist/component/schema.d.ts +368 -258
  47. package/dist/component/schema.js +23 -27
  48. package/dist/component/schema.js.map +1 -1
  49. package/dist/component/server/auth.d.ts +42 -7
  50. package/dist/component/server/auth.d.ts.map +1 -1
  51. package/dist/component/server/auth.js +70 -6
  52. package/dist/component/server/auth.js.map +1 -1
  53. package/dist/component/server/cookies.js +3 -0
  54. package/dist/component/server/cookies.js.map +1 -1
  55. package/dist/component/server/db.js +1 -0
  56. package/dist/component/server/db.js.map +1 -1
  57. package/dist/component/server/device.js +3 -1
  58. package/dist/component/server/device.js.map +1 -1
  59. package/dist/component/server/domains/core.js +466 -0
  60. package/dist/component/server/domains/core.js.map +1 -0
  61. package/dist/component/server/domains/sso.js +689 -0
  62. package/dist/component/server/domains/sso.js.map +1 -0
  63. package/dist/component/server/factory.d.ts +136 -0
  64. package/dist/component/server/factory.d.ts.map +1 -0
  65. package/dist/component/server/factory.js +1128 -0
  66. package/dist/component/server/factory.js.map +1 -0
  67. package/dist/component/server/fx.js +2 -1
  68. package/dist/component/server/fx.js.map +1 -1
  69. package/dist/component/server/http.js +287 -0
  70. package/dist/component/server/http.js.map +1 -0
  71. package/dist/component/server/identity.js +13 -0
  72. package/dist/component/server/identity.js.map +1 -0
  73. package/dist/component/server/keys.js +4 -0
  74. package/dist/component/server/keys.js.map +1 -1
  75. package/dist/component/server/mutations/account.js +1 -1
  76. package/dist/component/server/mutations/index.js +2 -2
  77. package/dist/component/server/mutations/index.js.map +1 -1
  78. package/dist/component/server/mutations/invalidate.js +1 -1
  79. package/dist/component/server/mutations/oauth.js +10 -7
  80. package/dist/component/server/mutations/oauth.js.map +1 -1
  81. package/dist/component/server/mutations/refresh.js +1 -1
  82. package/dist/component/server/mutations/register.js +1 -1
  83. package/dist/component/server/mutations/retrieve.js +1 -1
  84. package/dist/component/server/mutations/signature.js +1 -1
  85. package/dist/component/server/mutations/store.js +6 -3
  86. package/dist/component/server/mutations/store.js.map +1 -1
  87. package/dist/component/server/mutations/verify.js +1 -1
  88. package/dist/component/server/oauth.js +3 -0
  89. package/dist/component/server/oauth.js.map +1 -1
  90. package/dist/component/server/passkey.js +3 -2
  91. package/dist/component/server/passkey.js.map +1 -1
  92. package/dist/component/server/provider.js +2 -0
  93. package/dist/component/server/provider.js.map +1 -1
  94. package/dist/component/server/providers.js +3 -0
  95. package/dist/component/server/providers.js.map +1 -1
  96. package/dist/component/server/ratelimit.js +3 -0
  97. package/dist/component/server/ratelimit.js.map +1 -1
  98. package/dist/component/server/redirects.js +2 -0
  99. package/dist/component/server/redirects.js.map +1 -1
  100. package/dist/component/server/refresh.js +5 -0
  101. package/dist/component/server/refresh.js.map +1 -1
  102. package/dist/component/server/sessions.js +5 -0
  103. package/dist/component/server/sessions.js.map +1 -1
  104. package/dist/component/server/signin.js +2 -1
  105. package/dist/component/server/signin.js.map +1 -1
  106. package/dist/component/server/sso.js +166 -19
  107. package/dist/component/server/sso.js.map +1 -1
  108. package/dist/component/server/tokens.js +1 -0
  109. package/dist/component/server/tokens.js.map +1 -1
  110. package/dist/component/server/totp.js +4 -2
  111. package/dist/component/server/totp.js.map +1 -1
  112. package/dist/component/server/types.d.ts +50 -35
  113. package/dist/component/server/types.d.ts.map +1 -1
  114. package/dist/component/server/types.js.map +1 -1
  115. package/dist/component/server/users.js +1 -0
  116. package/dist/component/server/users.js.map +1 -1
  117. package/dist/component/server/utils.js +44 -2
  118. package/dist/component/server/utils.js.map +1 -1
  119. package/dist/providers/anonymous.d.ts +1 -1
  120. package/dist/providers/credentials.d.ts +1 -1
  121. package/dist/providers/password.d.ts +1 -1
  122. package/dist/providers/sso.d.ts +1 -1
  123. package/dist/providers/sso.js.map +1 -1
  124. package/dist/server/auth.d.ts +44 -9
  125. package/dist/server/auth.d.ts.map +1 -1
  126. package/dist/server/auth.js +70 -6
  127. package/dist/server/auth.js.map +1 -1
  128. package/dist/server/cookies.d.ts +1 -38
  129. package/dist/server/cookies.js +3 -0
  130. package/dist/server/cookies.js.map +1 -1
  131. package/dist/server/db.d.ts +1 -125
  132. package/dist/server/db.js +1 -0
  133. package/dist/server/db.js.map +1 -1
  134. package/dist/server/device.d.ts +1 -24
  135. package/dist/server/device.js +3 -1
  136. package/dist/server/device.js.map +1 -1
  137. package/dist/server/domains/core.d.ts +320 -0
  138. package/dist/server/domains/core.d.ts.map +1 -0
  139. package/dist/server/domains/core.js +466 -0
  140. package/dist/server/domains/core.js.map +1 -0
  141. package/dist/server/domains/sso.d.ts +340 -0
  142. package/dist/server/domains/sso.d.ts.map +1 -0
  143. package/dist/server/domains/sso.js +689 -0
  144. package/dist/server/domains/sso.js.map +1 -0
  145. package/dist/server/enterpriseValidators.d.ts +1 -0
  146. package/dist/server/enterpriseValidators.js +56 -0
  147. package/dist/server/enterpriseValidators.js.map +1 -0
  148. package/dist/server/factory.d.ts +136 -0
  149. package/dist/server/factory.d.ts.map +1 -0
  150. package/dist/server/factory.js +1128 -0
  151. package/dist/server/factory.js.map +1 -0
  152. package/dist/server/fx.d.ts +1 -16
  153. package/dist/server/fx.d.ts.map +1 -1
  154. package/dist/server/fx.js +1 -0
  155. package/dist/server/fx.js.map +1 -1
  156. package/dist/server/http.d.ts +59 -0
  157. package/dist/server/http.d.ts.map +1 -0
  158. package/dist/server/http.js +287 -0
  159. package/dist/server/http.js.map +1 -0
  160. package/dist/server/identity.d.ts +1 -0
  161. package/dist/server/identity.js +13 -0
  162. package/dist/server/identity.js.map +1 -0
  163. package/dist/server/index.d.ts +432 -1
  164. package/dist/server/index.d.ts.map +1 -1
  165. package/dist/server/index.js +486 -36
  166. package/dist/server/index.js.map +1 -1
  167. package/dist/server/keys.d.ts +1 -57
  168. package/dist/server/keys.js +4 -0
  169. package/dist/server/keys.js.map +1 -1
  170. package/dist/server/mutations/account.d.ts +7 -7
  171. package/dist/server/mutations/account.d.ts.map +1 -1
  172. package/dist/server/mutations/code.d.ts +13 -13
  173. package/dist/server/mutations/index.d.ts +107 -107
  174. package/dist/server/mutations/index.d.ts.map +1 -1
  175. package/dist/server/mutations/index.js +1 -1
  176. package/dist/server/mutations/index.js.map +1 -1
  177. package/dist/server/mutations/invalidate.d.ts +5 -5
  178. package/dist/server/mutations/oauth.d.ts +10 -10
  179. package/dist/server/mutations/oauth.d.ts.map +1 -1
  180. package/dist/server/mutations/oauth.js +9 -6
  181. package/dist/server/mutations/oauth.js.map +1 -1
  182. package/dist/server/mutations/refresh.d.ts +4 -4
  183. package/dist/server/mutations/register.d.ts +12 -12
  184. package/dist/server/mutations/register.d.ts.map +1 -1
  185. package/dist/server/mutations/retrieve.d.ts +1 -1
  186. package/dist/server/mutations/signature.d.ts +5 -5
  187. package/dist/server/mutations/signature.d.ts.map +1 -1
  188. package/dist/server/mutations/signin.d.ts +1 -1
  189. package/dist/server/mutations/signout.d.ts +1 -1
  190. package/dist/server/mutations/store.d.ts +3 -2
  191. package/dist/server/mutations/store.d.ts.map +1 -1
  192. package/dist/server/mutations/store.js +6 -3
  193. package/dist/server/mutations/store.js.map +1 -1
  194. package/dist/server/mutations/verifier.d.ts +1 -1
  195. package/dist/server/mutations/verify.d.ts +4 -4
  196. package/dist/server/oauth.d.ts +1 -59
  197. package/dist/server/oauth.js +3 -0
  198. package/dist/server/oauth.js.map +1 -1
  199. package/dist/server/passkey.d.ts.map +1 -1
  200. package/dist/server/passkey.js +3 -2
  201. package/dist/server/passkey.js.map +1 -1
  202. package/dist/server/provider.d.ts +1 -14
  203. package/dist/server/provider.d.ts.map +1 -1
  204. package/dist/server/provider.js +2 -0
  205. package/dist/server/provider.js.map +1 -1
  206. package/dist/server/providers.js +3 -0
  207. package/dist/server/providers.js.map +1 -1
  208. package/dist/server/ratelimit.d.ts +1 -22
  209. package/dist/server/ratelimit.js +3 -0
  210. package/dist/server/ratelimit.js.map +1 -1
  211. package/dist/server/redirects.d.ts +1 -10
  212. package/dist/server/redirects.js +2 -0
  213. package/dist/server/redirects.js.map +1 -1
  214. package/dist/server/refresh.d.ts +1 -37
  215. package/dist/server/refresh.js +5 -0
  216. package/dist/server/refresh.js.map +1 -1
  217. package/dist/server/sessions.d.ts +1 -28
  218. package/dist/server/sessions.js +5 -0
  219. package/dist/server/sessions.js.map +1 -1
  220. package/dist/server/signin.d.ts +1 -55
  221. package/dist/server/signin.js +2 -1
  222. package/dist/server/signin.js.map +1 -1
  223. package/dist/server/sso.d.ts +1 -348
  224. package/dist/server/sso.js +165 -18
  225. package/dist/server/sso.js.map +1 -1
  226. package/dist/server/templates.d.ts +1 -21
  227. package/dist/server/templates.js +1 -0
  228. package/dist/server/templates.js.map +1 -1
  229. package/dist/server/tokens.d.ts +1 -11
  230. package/dist/server/tokens.js +1 -0
  231. package/dist/server/tokens.js.map +1 -1
  232. package/dist/server/totp.d.ts +1 -23
  233. package/dist/server/totp.js +4 -2
  234. package/dist/server/totp.js.map +1 -1
  235. package/dist/server/types.d.ts +55 -71
  236. package/dist/server/types.d.ts.map +1 -1
  237. package/dist/server/types.js.map +1 -1
  238. package/dist/server/users.d.ts +1 -31
  239. package/dist/server/users.js +1 -0
  240. package/dist/server/users.js.map +1 -1
  241. package/dist/server/utils.d.ts +1 -27
  242. package/dist/server/utils.js +44 -2
  243. package/dist/server/utils.js.map +1 -1
  244. package/dist/server/version.d.ts +1 -1
  245. package/dist/server/version.js +1 -1
  246. package/dist/server/version.js.map +1 -1
  247. package/package.json +4 -5
  248. package/src/cli/bin.ts +5 -0
  249. package/src/cli/index.ts +22 -9
  250. package/src/cli/keys.ts +3 -0
  251. package/src/client/index.ts +36 -37
  252. package/src/component/_generated/api.ts +14 -0
  253. package/src/component/_generated/component.ts +1920 -3
  254. package/src/component/index.ts +2 -0
  255. package/src/component/model.ts +424 -0
  256. package/src/component/public/enterprise.ts +654 -0
  257. package/src/component/public/factors.ts +332 -0
  258. package/src/component/public/groups.ts +951 -0
  259. package/src/component/public/identity.ts +566 -0
  260. package/src/component/public/keys.ts +209 -0
  261. package/src/component/public/shared.ts +117 -0
  262. package/src/component/public.ts +5 -2965
  263. package/src/component/schema.ts +47 -57
  264. package/src/providers/sso.ts +1 -1
  265. package/src/server/auth.ts +192 -9
  266. package/src/server/cookies.ts +3 -0
  267. package/src/server/db.ts +3 -0
  268. package/src/server/device.ts +3 -1
  269. package/src/server/domains/core.ts +916 -0
  270. package/src/server/domains/sso.ts +1462 -0
  271. package/src/server/enterpriseValidators.ts +88 -0
  272. package/src/server/factory.ts +2168 -0
  273. package/src/server/fx.ts +1 -0
  274. package/src/server/http.ts +529 -0
  275. package/src/server/identity.ts +18 -0
  276. package/src/server/index.ts +712 -40
  277. package/src/server/keys.ts +4 -0
  278. package/src/server/mutations/index.ts +1 -1
  279. package/src/server/mutations/oauth.ts +36 -8
  280. package/src/server/mutations/store.ts +6 -3
  281. package/src/server/oauth.ts +6 -0
  282. package/src/server/passkey.ts +3 -2
  283. package/src/server/provider.ts +2 -0
  284. package/src/server/providers.ts +3 -0
  285. package/src/server/ratelimit.ts +3 -0
  286. package/src/server/redirects.ts +2 -0
  287. package/src/server/refresh.ts +5 -0
  288. package/src/server/sessions.ts +5 -0
  289. package/src/server/signin.ts +1 -0
  290. package/src/server/sso.ts +251 -17
  291. package/src/server/templates.ts +1 -0
  292. package/src/server/tokens.ts +1 -0
  293. package/src/server/totp.ts +4 -2
  294. package/src/server/types.ts +85 -77
  295. package/src/server/users.ts +1 -0
  296. package/src/server/utils.ts +71 -1
  297. package/src/server/version.ts +1 -1
  298. package/dist/component/public.js.map +0 -1
  299. package/dist/component/server/implementation.d.ts +0 -1264
  300. package/dist/component/server/implementation.d.ts.map +0 -1
  301. package/dist/component/server/implementation.js +0 -2365
  302. package/dist/component/server/implementation.js.map +0 -1
  303. package/dist/server/cookies.d.ts.map +0 -1
  304. package/dist/server/db.d.ts.map +0 -1
  305. package/dist/server/device.d.ts.map +0 -1
  306. package/dist/server/implementation.d.ts +0 -1264
  307. package/dist/server/implementation.d.ts.map +0 -1
  308. package/dist/server/implementation.js +0 -2365
  309. package/dist/server/implementation.js.map +0 -1
  310. package/dist/server/keys.d.ts.map +0 -1
  311. package/dist/server/oauth.d.ts.map +0 -1
  312. package/dist/server/ratelimit.d.ts.map +0 -1
  313. package/dist/server/redirects.d.ts.map +0 -1
  314. package/dist/server/refresh.d.ts.map +0 -1
  315. package/dist/server/sessions.d.ts.map +0 -1
  316. package/dist/server/signin.d.ts.map +0 -1
  317. package/dist/server/sso.d.ts.map +0 -1
  318. package/dist/server/templates.d.ts.map +0 -1
  319. package/dist/server/tokens.d.ts.map +0 -1
  320. package/dist/server/totp.d.ts.map +0 -1
  321. package/dist/server/users.d.ts.map +0 -1
  322. package/dist/server/utils.d.ts.map +0 -1
  323. package/src/server/implementation.ts +0 -5336
@@ -0,0 +1,599 @@
1
+ import { mutation, query } from "../functions.js";
2
+ import { vGroupDoc, vGroupInviteDoc, vGroupMemberDoc, vInviteAcceptByTokenResult, vInviteStatus, vTag } from "../model.js";
3
+ import { ConvexError, normalizeTag, normalizeTags, v, vPaginated } from "./shared.js";
4
+
5
+ //#region src/component/public/groups.ts
6
+ /**
7
+ * Create a new group. Groups are hierarchical — set `parentGroupId` to nest
8
+ * under an existing group, or omit it to create a root-level group.
9
+ *
10
+ * @returns The ID of the newly created group.
11
+ */
12
+ const groupCreate = mutation({
13
+ args: {
14
+ name: v.string(),
15
+ slug: v.optional(v.string()),
16
+ type: v.optional(v.string()),
17
+ parentGroupId: v.optional(v.id("Group")),
18
+ tags: v.optional(v.array(vTag)),
19
+ extend: v.optional(v.any())
20
+ },
21
+ returns: v.id("Group"),
22
+ handler: async (ctx, args) => {
23
+ const { tags: rawTags, ...rest } = args;
24
+ const normalizedTags = rawTags ? normalizeTags(rawTags) : void 0;
25
+ const groupId = await ctx.db.insert("Group", {
26
+ ...rest,
27
+ tags: normalizedTags
28
+ });
29
+ if (normalizedTags) for (const tag of normalizedTags) await ctx.db.insert("GroupTag", {
30
+ group_id: groupId,
31
+ key: tag.key,
32
+ value: tag.value
33
+ });
34
+ return groupId;
35
+ }
36
+ });
37
+ /** Retrieve a group by its document ID. Returns `null` if not found. */
38
+ const groupGet = query({
39
+ args: { groupId: v.id("Group") },
40
+ returns: v.union(vGroupDoc, v.null()),
41
+ handler: async (ctx, { groupId }) => {
42
+ return await ctx.db.get("Group", groupId);
43
+ }
44
+ });
45
+ /**
46
+ * List groups with optional filtering, sorting, and pagination.
47
+ *
48
+ * Returns `{ items, nextCursor }`. Empty `where` returns **all** groups.
49
+ */
50
+ const groupList = query({
51
+ args: {
52
+ where: v.optional(v.object({
53
+ slug: v.optional(v.string()),
54
+ type: v.optional(v.string()),
55
+ parentGroupId: v.optional(v.id("Group")),
56
+ name: v.optional(v.string()),
57
+ isRoot: v.optional(v.boolean()),
58
+ tagsAll: v.optional(v.array(vTag)),
59
+ tagsAny: v.optional(v.array(vTag))
60
+ })),
61
+ limit: v.optional(v.number()),
62
+ cursor: v.optional(v.union(v.string(), v.null())),
63
+ orderBy: v.optional(v.union(v.literal("_creationTime"), v.literal("name"), v.literal("slug"), v.literal("type"))),
64
+ order: v.optional(v.union(v.literal("asc"), v.literal("desc")))
65
+ },
66
+ returns: vPaginated(vGroupDoc),
67
+ handler: async (ctx, args) => {
68
+ const where = args.where ?? {};
69
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);
70
+ const order = args.order ?? "desc";
71
+ let tagFilteredIds = null;
72
+ if (where.tagsAll && where.tagsAll.length > 0) {
73
+ let allSet = null;
74
+ for (const rawTag of where.tagsAll) {
75
+ const t = normalizeTag(rawTag);
76
+ const rows = await ctx.db.query("GroupTag").withIndex("by_key_value", (idx) => idx.eq("key", t.key).eq("value", t.value)).collect();
77
+ const ids = new Set(rows.map((r) => r.group_id));
78
+ if (allSet === null) allSet = ids;
79
+ else for (const id of allSet) if (!ids.has(id)) allSet.delete(id);
80
+ if (allSet.size === 0) break;
81
+ }
82
+ tagFilteredIds = allSet ?? /* @__PURE__ */ new Set();
83
+ }
84
+ if (where.tagsAny && where.tagsAny.length > 0) {
85
+ const anySet = /* @__PURE__ */ new Set();
86
+ for (const rawTag of where.tagsAny) {
87
+ const t = normalizeTag(rawTag);
88
+ const rows = await ctx.db.query("GroupTag").withIndex("by_key_value", (idx) => idx.eq("key", t.key).eq("value", t.value)).collect();
89
+ for (const r of rows) anySet.add(r.group_id);
90
+ }
91
+ if (tagFilteredIds !== null) {
92
+ for (const id of tagFilteredIds) if (!anySet.has(id)) tagFilteredIds.delete(id);
93
+ } else tagFilteredIds = anySet;
94
+ }
95
+ let q;
96
+ if (where.type !== void 0 && where.parentGroupId !== void 0) q = ctx.db.query("Group").withIndex("type_parent_group_id", (idx) => idx.eq("type", where.type).eq("parentGroupId", where.parentGroupId));
97
+ else if (where.slug !== void 0) q = ctx.db.query("Group").withIndex("slug", (idx) => idx.eq("slug", where.slug));
98
+ else if (where.type !== void 0) q = ctx.db.query("Group").withIndex("type", (idx) => idx.eq("type", where.type));
99
+ else if (where.parentGroupId !== void 0) q = ctx.db.query("Group").withIndex("parent_group_id", (idx) => idx.eq("parentGroupId", where.parentGroupId));
100
+ else q = ctx.db.query("Group");
101
+ if (where.name !== void 0) q = q.filter((f) => f.eq(f.field("name"), where.name));
102
+ if (where.isRoot === true) q = q.filter((f) => f.eq(f.field("parentGroupId"), void 0));
103
+ else if (where.isRoot === false) q = q.filter((f) => f.neq(f.field("parentGroupId"), void 0));
104
+ if (where.slug !== void 0 && where.type !== void 0) q = q.filter((f) => f.eq(f.field("slug"), where.slug));
105
+ q = q.order(order);
106
+ let all = await q.collect();
107
+ if (tagFilteredIds !== null) all = all.filter((doc) => tagFilteredIds.has(doc._id));
108
+ let startIdx = 0;
109
+ if (args.cursor) {
110
+ const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);
111
+ if (cursorIdx !== -1) startIdx = cursorIdx + 1;
112
+ }
113
+ const page = all.slice(startIdx, startIdx + limit + 1);
114
+ const hasMore = page.length > limit;
115
+ const items = hasMore ? page.slice(0, limit) : page;
116
+ return {
117
+ items,
118
+ nextCursor: hasMore ? items[items.length - 1]._id : null
119
+ };
120
+ }
121
+ });
122
+ /** Update a group's fields (name, slug, tags, extend, parentGroupId). */
123
+ const groupUpdate = mutation({
124
+ args: {
125
+ groupId: v.id("Group"),
126
+ data: v.any()
127
+ },
128
+ returns: v.null(),
129
+ handler: async (ctx, { groupId, data }) => {
130
+ if (data.tags !== void 0) {
131
+ const normalizedTags = Array.isArray(data.tags) ? normalizeTags(data.tags) : [];
132
+ const existingTags = await ctx.db.query("GroupTag").withIndex("by_group", (idx) => idx.eq("group_id", groupId)).collect();
133
+ for (const existing of existingTags) await ctx.db.delete("GroupTag", existing._id);
134
+ for (const tag of normalizedTags) await ctx.db.insert("GroupTag", {
135
+ group_id: groupId,
136
+ key: tag.key,
137
+ value: tag.value
138
+ });
139
+ await ctx.db.patch("Group", groupId, {
140
+ ...data,
141
+ tags: normalizedTags.length > 0 ? normalizedTags : void 0
142
+ });
143
+ } else await ctx.db.patch("Group", groupId, data);
144
+ return null;
145
+ }
146
+ });
147
+ /**
148
+ * Delete a group and all of its descendants. This cascades to:
149
+ * - All child groups (recursively)
150
+ * - All members of this group and its descendants
151
+ * - All invites for this group and its descendants
152
+ */
153
+ const groupDelete = mutation({
154
+ args: { groupId: v.id("Group") },
155
+ returns: v.null(),
156
+ handler: async (ctx, { groupId }) => {
157
+ const deleteGroup = async (id) => {
158
+ const children = await ctx.db.query("Group").withIndex("parent_group_id", (q) => q.eq("parentGroupId", id)).collect();
159
+ for (const child of children) await deleteGroup(child._id);
160
+ const members = await ctx.db.query("GroupMember").withIndex("group_id", (q) => q.eq("groupId", id)).collect();
161
+ for (const member of members) await ctx.db.delete("GroupMember", member._id);
162
+ const invites = await ctx.db.query("GroupInvite").withIndex("group_id", (q) => q.eq("groupId", id)).collect();
163
+ for (const invite of invites) await ctx.db.delete("GroupInvite", invite._id);
164
+ const tags = await ctx.db.query("GroupTag").withIndex("by_group", (q) => q.eq("group_id", id)).collect();
165
+ for (const tag of tags) await ctx.db.delete("GroupTag", tag._id);
166
+ await ctx.db.delete("Group", id);
167
+ };
168
+ await deleteGroup(groupId);
169
+ return null;
170
+ }
171
+ });
172
+ /**
173
+ * Add a user as a member of a group.
174
+ *
175
+ * The `role` field is an application-defined string (e.g. "owner", "admin",
176
+ * "member", "viewer"). The auth component stores it but does not enforce
177
+ * access control — your application defines what each role means.
178
+ *
179
+ * Throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` when the user is
180
+ * already a member of the target group.
181
+ *
182
+ * @returns The ID of the new member record.
183
+ */
184
+ const memberAdd = mutation({
185
+ args: {
186
+ groupId: v.id("Group"),
187
+ userId: v.id("User"),
188
+ role: v.optional(v.string()),
189
+ status: v.optional(v.string()),
190
+ extend: v.optional(v.any())
191
+ },
192
+ returns: v.id("GroupMember"),
193
+ handler: async (ctx, args) => {
194
+ const existingMembership = await ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", args.groupId).eq("userId", args.userId)).unique();
195
+ if (existingMembership !== null) throw new ConvexError({
196
+ code: "DUPLICATE_MEMBERSHIP",
197
+ message: "User is already a member of this group",
198
+ groupId: args.groupId,
199
+ userId: args.userId,
200
+ existingMemberId: existingMembership._id
201
+ });
202
+ return await ctx.db.insert("GroupMember", args);
203
+ }
204
+ });
205
+ /** Retrieve a member record by its document ID. Returns `null` if not found. */
206
+ const memberGet = query({
207
+ args: { memberId: v.id("GroupMember") },
208
+ returns: v.union(vGroupMemberDoc, v.null()),
209
+ handler: async (ctx, { memberId }) => {
210
+ return await ctx.db.get("GroupMember", memberId);
211
+ }
212
+ });
213
+ /**
214
+ * List members with optional filtering, sorting, and pagination.
215
+ *
216
+ * Returns `{ items, nextCursor }`. Supports filtering by `groupId`,
217
+ * `userId`, `role`, and `status`.
218
+ */
219
+ const memberList = query({
220
+ args: {
221
+ where: v.optional(v.object({
222
+ groupId: v.optional(v.id("Group")),
223
+ userId: v.optional(v.id("User")),
224
+ role: v.optional(v.string()),
225
+ status: v.optional(v.string())
226
+ })),
227
+ limit: v.optional(v.number()),
228
+ cursor: v.optional(v.union(v.string(), v.null())),
229
+ orderBy: v.optional(v.union(v.literal("_creationTime"), v.literal("role"), v.literal("status"))),
230
+ order: v.optional(v.union(v.literal("asc"), v.literal("desc")))
231
+ },
232
+ returns: vPaginated(vGroupMemberDoc),
233
+ handler: async (ctx, args) => {
234
+ const where = args.where ?? {};
235
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);
236
+ const order = args.order ?? "desc";
237
+ let q;
238
+ if (where.groupId !== void 0 && where.userId !== void 0) q = ctx.db.query("GroupMember").withIndex("group_id_user_id", (idx) => idx.eq("groupId", where.groupId).eq("userId", where.userId));
239
+ else if (where.groupId !== void 0) q = ctx.db.query("GroupMember").withIndex("group_id", (idx) => idx.eq("groupId", where.groupId));
240
+ else if (where.userId !== void 0) q = ctx.db.query("GroupMember").withIndex("user_id", (idx) => idx.eq("userId", where.userId));
241
+ else q = ctx.db.query("GroupMember");
242
+ if (where.role !== void 0) q = q.filter((f) => f.eq(f.field("role"), where.role));
243
+ if (where.status !== void 0) q = q.filter((f) => f.eq(f.field("status"), where.status));
244
+ q = q.order(order);
245
+ const all = await q.collect();
246
+ let startIdx = 0;
247
+ if (args.cursor) {
248
+ const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);
249
+ if (cursorIdx !== -1) startIdx = cursorIdx + 1;
250
+ }
251
+ const page = all.slice(startIdx, startIdx + limit + 1);
252
+ const hasMore = page.length > limit;
253
+ const items = hasMore ? page.slice(0, limit) : page;
254
+ return {
255
+ items,
256
+ nextCursor: hasMore ? items[items.length - 1]._id : null
257
+ };
258
+ }
259
+ });
260
+ /**
261
+ * @deprecated Use `memberList` with `where: { userId }` instead.
262
+ * Kept for backward compatibility with generated component types.
263
+ */
264
+ const memberListByUser = query({
265
+ args: { userId: v.id("User") },
266
+ returns: v.array(vGroupMemberDoc),
267
+ handler: async (ctx, { userId }) => {
268
+ return await ctx.db.query("GroupMember").withIndex("user_id", (q) => q.eq("userId", userId)).collect();
269
+ }
270
+ });
271
+ /**
272
+ * Look up a specific user's membership in a specific group.
273
+ * Returns `null` if the user is not a member of the group.
274
+ */
275
+ const memberGetByGroupAndUser = query({
276
+ args: {
277
+ groupId: v.id("Group"),
278
+ userId: v.id("User")
279
+ },
280
+ returns: v.union(vGroupMemberDoc, v.null()),
281
+ handler: async (ctx, { groupId, userId }) => {
282
+ return await ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", groupId).eq("userId", userId)).unique();
283
+ }
284
+ });
285
+ /** Remove a member from a group by deleting the member record. */
286
+ const memberRemove = mutation({
287
+ args: { memberId: v.id("GroupMember") },
288
+ returns: v.null(),
289
+ handler: async (ctx, { memberId }) => {
290
+ await ctx.db.delete("GroupMember", memberId);
291
+ return null;
292
+ }
293
+ });
294
+ /**
295
+ * Update a member record's fields (role, status, extend).
296
+ *
297
+ * Common usage: `memberUpdate({ memberId, data: { role: "admin" } })`
298
+ */
299
+ const memberUpdate = mutation({
300
+ args: {
301
+ memberId: v.id("GroupMember"),
302
+ data: v.any()
303
+ },
304
+ returns: v.null(),
305
+ handler: async (ctx, { memberId, data }) => {
306
+ await ctx.db.patch("GroupMember", memberId, data);
307
+ return null;
308
+ }
309
+ });
310
+ /**
311
+ * Create a new platform-level invitation. Optionally set `groupId` to tie
312
+ * the invite to a specific group. The invitation is sent to an email address
313
+ * and includes a hashed token for secure acceptance.
314
+ *
315
+ * Throws `ConvexError` with code `DUPLICATE_INVITE` when a pending invite
316
+ * already exists for the same email and scope:
317
+ * - group invite: same `email` + same `groupId`
318
+ * - platform invite: same `email` with no `groupId`
319
+ *
320
+ * @returns The ID of the new invite record.
321
+ */
322
+ const inviteCreate = mutation({
323
+ args: {
324
+ groupId: v.optional(v.id("Group")),
325
+ invitedByUserId: v.optional(v.id("User")),
326
+ email: v.optional(v.string()),
327
+ tokenHash: v.string(),
328
+ role: v.optional(v.string()),
329
+ status: vInviteStatus,
330
+ expiresTime: v.optional(v.number()),
331
+ extend: v.optional(v.any())
332
+ },
333
+ returns: v.id("GroupInvite"),
334
+ handler: async (ctx, args) => {
335
+ const now = Date.now();
336
+ if (args.email !== void 0) if (args.groupId !== void 0) {
337
+ const existingGroupInvites = await ctx.db.query("GroupInvite").withIndex("group_id_status", (q) => q.eq("groupId", args.groupId).eq("status", "pending")).filter((q) => q.eq(q.field("email"), args.email)).collect();
338
+ for (const existingGroupInvite of existingGroupInvites) {
339
+ if (existingGroupInvite.expiresTime !== void 0 && existingGroupInvite.expiresTime <= now) {
340
+ await ctx.db.patch("GroupInvite", existingGroupInvite._id, { status: "expired" });
341
+ continue;
342
+ }
343
+ throw new ConvexError({
344
+ code: "DUPLICATE_INVITE",
345
+ message: "A pending invite already exists for this email in this group",
346
+ email: args.email,
347
+ groupId: args.groupId,
348
+ existingInviteId: existingGroupInvite._id
349
+ });
350
+ }
351
+ } else {
352
+ const existingPlatformInvites = await ctx.db.query("GroupInvite").withIndex("email_status", (q) => q.eq("email", args.email).eq("status", "pending")).filter((q) => q.eq(q.field("groupId"), void 0)).collect();
353
+ for (const existingPlatformInvite of existingPlatformInvites) {
354
+ if (existingPlatformInvite.expiresTime !== void 0 && existingPlatformInvite.expiresTime <= now) {
355
+ await ctx.db.patch("GroupInvite", existingPlatformInvite._id, { status: "expired" });
356
+ continue;
357
+ }
358
+ throw new ConvexError({
359
+ code: "DUPLICATE_INVITE",
360
+ message: "A pending platform invite already exists for this email",
361
+ email: args.email,
362
+ existingInviteId: existingPlatformInvite._id
363
+ });
364
+ }
365
+ }
366
+ return await ctx.db.insert("GroupInvite", args);
367
+ }
368
+ });
369
+ /** Retrieve an invite by its document ID. Returns `null` if not found. */
370
+ const inviteGet = query({
371
+ args: { inviteId: v.id("GroupInvite") },
372
+ returns: v.union(vGroupInviteDoc, v.null()),
373
+ handler: async (ctx, { inviteId }) => {
374
+ return await ctx.db.get("GroupInvite", inviteId);
375
+ }
376
+ });
377
+ /** Retrieve an invite by hashed token. Returns `null` if not found. */
378
+ const inviteGetByTokenHash = query({
379
+ args: { tokenHash: v.string() },
380
+ returns: v.union(vGroupInviteDoc, v.null()),
381
+ handler: async (ctx, { tokenHash }) => {
382
+ return await ctx.db.query("GroupInvite").withIndex("token_hash", (q) => q.eq("tokenHash", tokenHash)).first();
383
+ }
384
+ });
385
+ /**
386
+ * List invites with optional filtering, sorting, and pagination.
387
+ *
388
+ * Returns `{ items, nextCursor }`. Supports filtering by `groupId`,
389
+ * `status`, `email`, `invitedByUserId`, `role`, `acceptedByUserId`, and `tokenHash`.
390
+ */
391
+ const inviteList = query({
392
+ args: {
393
+ where: v.optional(v.object({
394
+ tokenHash: v.optional(v.string()),
395
+ groupId: v.optional(v.id("Group")),
396
+ status: v.optional(vInviteStatus),
397
+ email: v.optional(v.string()),
398
+ invitedByUserId: v.optional(v.id("User")),
399
+ role: v.optional(v.string()),
400
+ acceptedByUserId: v.optional(v.id("User"))
401
+ })),
402
+ limit: v.optional(v.number()),
403
+ cursor: v.optional(v.union(v.string(), v.null())),
404
+ orderBy: v.optional(v.union(v.literal("_creationTime"), v.literal("status"), v.literal("email"), v.literal("expiresTime"), v.literal("acceptedTime"))),
405
+ order: v.optional(v.union(v.literal("asc"), v.literal("desc")))
406
+ },
407
+ returns: vPaginated(vGroupInviteDoc),
408
+ handler: async (ctx, args) => {
409
+ const where = args.where ?? {};
410
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);
411
+ const order = args.order ?? "desc";
412
+ let q;
413
+ if (where.tokenHash !== void 0) q = ctx.db.query("GroupInvite").withIndex("token_hash", (idx) => idx.eq("tokenHash", where.tokenHash));
414
+ else if (where.role !== void 0 && where.status !== void 0 && where.acceptedByUserId !== void 0) q = ctx.db.query("GroupInvite").withIndex("role_status_accepted_by_user_id", (idx) => idx.eq("role", where.role).eq("status", where.status).eq("acceptedByUserId", where.acceptedByUserId));
415
+ else if (where.groupId !== void 0 && where.status !== void 0) q = ctx.db.query("GroupInvite").withIndex("group_id_status", (idx) => idx.eq("groupId", where.groupId).eq("status", where.status));
416
+ else if (where.email !== void 0 && where.status !== void 0) q = ctx.db.query("GroupInvite").withIndex("email_status", (idx) => idx.eq("email", where.email).eq("status", where.status));
417
+ else if (where.invitedByUserId !== void 0 && where.status !== void 0) q = ctx.db.query("GroupInvite").withIndex("invited_by_user_id_status", (idx) => idx.eq("invitedByUserId", where.invitedByUserId).eq("status", where.status));
418
+ else if (where.groupId !== void 0) q = ctx.db.query("GroupInvite").withIndex("group_id", (idx) => idx.eq("groupId", where.groupId));
419
+ else if (where.status !== void 0) q = ctx.db.query("GroupInvite").withIndex("status", (idx) => idx.eq("status", where.status));
420
+ else q = ctx.db.query("GroupInvite");
421
+ if (where.groupId !== void 0) q = q.filter((f) => f.eq(f.field("groupId"), where.groupId));
422
+ if (where.status !== void 0) q = q.filter((f) => f.eq(f.field("status"), where.status));
423
+ if (where.email !== void 0) q = q.filter((f) => f.eq(f.field("email"), where.email));
424
+ if (where.invitedByUserId !== void 0) q = q.filter((f) => f.eq(f.field("invitedByUserId"), where.invitedByUserId));
425
+ if (where.role !== void 0) q = q.filter((f) => f.eq(f.field("role"), where.role));
426
+ if (where.acceptedByUserId !== void 0) q = q.filter((f) => f.eq(f.field("acceptedByUserId"), where.acceptedByUserId));
427
+ if (where.tokenHash !== void 0) q = q.filter((f) => f.eq(f.field("tokenHash"), where.tokenHash));
428
+ q = q.order(order);
429
+ const all = await q.collect();
430
+ let startIdx = 0;
431
+ if (args.cursor) {
432
+ const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);
433
+ if (cursorIdx !== -1) startIdx = cursorIdx + 1;
434
+ }
435
+ const page = all.slice(startIdx, startIdx + limit + 1);
436
+ const hasMore = page.length > limit;
437
+ const items = hasMore ? page.slice(0, limit) : page;
438
+ return {
439
+ items,
440
+ nextCursor: hasMore ? items[items.length - 1]._id : null
441
+ };
442
+ }
443
+ });
444
+ /**
445
+ * Accept a pending invitation.
446
+ *
447
+ * Marks the invite as "accepted" and records the acceptance timestamp.
448
+ * Throws a structured `ConvexError` when the invite doesn't exist or is not
449
+ * currently pending.
450
+ *
451
+ * The caller is responsible for creating the corresponding member record.
452
+ */
453
+ const inviteAccept = mutation({
454
+ args: {
455
+ inviteId: v.id("GroupInvite"),
456
+ acceptedByUserId: v.optional(v.id("User"))
457
+ },
458
+ returns: v.null(),
459
+ handler: async (ctx, { inviteId, acceptedByUserId }) => {
460
+ const invite = await ctx.db.get("GroupInvite", inviteId);
461
+ if (invite === null) throw new ConvexError({
462
+ code: "INVITE_NOT_FOUND",
463
+ message: "Invite not found",
464
+ inviteId
465
+ });
466
+ if (invite.status !== "pending") throw new ConvexError({
467
+ code: "INVITE_NOT_PENDING",
468
+ message: `Cannot accept invite with status "${invite.status}"`,
469
+ inviteId,
470
+ currentStatus: invite.status
471
+ });
472
+ if (invite.expiresTime !== void 0 && invite.expiresTime <= Date.now()) {
473
+ await ctx.db.patch("GroupInvite", inviteId, { status: "expired" });
474
+ throw new ConvexError({
475
+ code: "INVITE_EXPIRED",
476
+ message: "Invite has expired",
477
+ inviteId
478
+ });
479
+ }
480
+ await ctx.db.patch("GroupInvite", inviteId, {
481
+ status: "accepted",
482
+ acceptedTime: Date.now(),
483
+ ...acceptedByUserId ? { acceptedByUserId } : {}
484
+ });
485
+ return null;
486
+ }
487
+ });
488
+ /**
489
+ * Accept an invitation by raw token hash and atomically join group membership.
490
+ *
491
+ * Returns idempotent success when the invite was already accepted by the same
492
+ * user. If the invite targets a group, this mutation also ensures membership.
493
+ */
494
+ const inviteAcceptByToken = mutation({
495
+ args: {
496
+ tokenHash: v.string(),
497
+ acceptedByUserId: v.id("User")
498
+ },
499
+ returns: vInviteAcceptByTokenResult,
500
+ handler: async (ctx, { tokenHash, acceptedByUserId }) => {
501
+ const invite = await ctx.db.query("GroupInvite").withIndex("token_hash", (q) => q.eq("tokenHash", tokenHash)).first();
502
+ if (invite === null) throw new ConvexError({
503
+ code: "INVITE_NOT_FOUND",
504
+ message: "Invite not found"
505
+ });
506
+ const now = Date.now();
507
+ if (invite.status === "pending") {
508
+ if (invite.expiresTime !== void 0 && invite.expiresTime <= now) {
509
+ await ctx.db.patch("GroupInvite", invite._id, { status: "expired" });
510
+ throw new ConvexError({
511
+ code: "INVITE_EXPIRED",
512
+ message: "Invite has expired",
513
+ inviteId: invite._id
514
+ });
515
+ }
516
+ } else if (invite.status === "accepted") {
517
+ if (invite.acceptedByUserId !== acceptedByUserId) throw new ConvexError({
518
+ code: "INVITE_ALREADY_ACCEPTED",
519
+ message: "Invite already accepted by another user",
520
+ inviteId: invite._id
521
+ });
522
+ } else throw new ConvexError({
523
+ code: "INVITE_NOT_PENDING",
524
+ message: `Cannot accept invite with status "${invite.status}"`,
525
+ inviteId: invite._id,
526
+ currentStatus: invite.status
527
+ });
528
+ if (invite.email !== void 0) {
529
+ const user = await ctx.db.get("User", acceptedByUserId);
530
+ const normalizedInviteEmail = invite.email.trim().toLowerCase();
531
+ const normalizedUserEmail = user?.email?.trim().toLowerCase();
532
+ if (normalizedUserEmail === void 0 || normalizedUserEmail !== normalizedInviteEmail) throw new ConvexError({
533
+ code: "INVITE_EMAIL_MISMATCH",
534
+ message: "Invite email does not match accepting user's email",
535
+ inviteId: invite._id
536
+ });
537
+ }
538
+ let membershipStatus = "not_applicable";
539
+ let memberId;
540
+ if (invite.groupId !== void 0) {
541
+ const existingMembership = await ctx.db.query("GroupMember").withIndex("group_id_user_id", (q) => q.eq("groupId", invite.groupId).eq("userId", acceptedByUserId)).unique();
542
+ if (existingMembership !== null) {
543
+ membershipStatus = "already_joined";
544
+ memberId = existingMembership._id;
545
+ } else {
546
+ memberId = await ctx.db.insert("GroupMember", {
547
+ groupId: invite.groupId,
548
+ userId: acceptedByUserId,
549
+ role: invite.role,
550
+ status: "active"
551
+ });
552
+ membershipStatus = "joined";
553
+ }
554
+ }
555
+ if (invite.status === "pending") await ctx.db.patch("GroupInvite", invite._id, {
556
+ status: "accepted",
557
+ acceptedByUserId,
558
+ acceptedTime: now
559
+ });
560
+ const inviteStatus = invite.status === "accepted" ? "already_accepted" : "accepted";
561
+ return {
562
+ inviteId: invite._id,
563
+ groupId: invite.groupId ?? null,
564
+ memberId,
565
+ inviteStatus,
566
+ membershipStatus
567
+ };
568
+ }
569
+ });
570
+ /**
571
+ * Revoke a pending invitation.
572
+ *
573
+ * Marks the invite as "revoked". Throws a structured `ConvexError` when the
574
+ * invite doesn't exist or is not currently pending.
575
+ */
576
+ const inviteRevoke = mutation({
577
+ args: { inviteId: v.id("GroupInvite") },
578
+ returns: v.null(),
579
+ handler: async (ctx, { inviteId }) => {
580
+ const invite = await ctx.db.get("GroupInvite", inviteId);
581
+ if (invite === null) throw new ConvexError({
582
+ code: "INVITE_NOT_FOUND",
583
+ message: "Invite not found",
584
+ inviteId
585
+ });
586
+ if (invite.status !== "pending") throw new ConvexError({
587
+ code: "INVITE_NOT_PENDING",
588
+ message: `Cannot revoke invite with status "${invite.status}"`,
589
+ inviteId,
590
+ currentStatus: invite.status
591
+ });
592
+ await ctx.db.patch("GroupInvite", inviteId, { status: "revoked" });
593
+ return null;
594
+ }
595
+ });
596
+
597
+ //#endregion
598
+ export { groupCreate, groupDelete, groupGet, groupList, groupUpdate, inviteAccept, inviteAcceptByToken, inviteCreate, inviteGet, inviteGetByTokenHash, inviteList, inviteRevoke, memberAdd, memberGet, memberGetByGroupAndUser, memberList, memberListByUser, memberRemove, memberUpdate };
599
+ //# sourceMappingURL=groups.js.map