@robelest/convex-auth 0.0.4-preview.22 → 0.0.4-preview.23

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 (306) hide show
  1. package/dist/authorization/index.d.ts +1 -1
  2. package/dist/authorization/index.js +1 -1
  3. package/dist/authorization/index.js.map +1 -1
  4. package/dist/client/index.d.ts +1 -2
  5. package/dist/client/index.d.ts.map +1 -1
  6. package/dist/client/index.js +36 -39
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/component/client/index.d.ts +1 -2
  9. package/dist/component/model.d.ts +9 -9
  10. package/dist/component/model.d.ts.map +1 -1
  11. package/dist/component/public/enterprise/audit.d.ts.map +1 -1
  12. package/dist/component/public/enterprise/audit.js.map +1 -1
  13. package/dist/component/public/enterprise/core.d.ts.map +1 -1
  14. package/dist/component/public/enterprise/core.js.map +1 -1
  15. package/dist/component/public/enterprise/domains.d.ts.map +1 -1
  16. package/dist/component/public/enterprise/domains.js.map +1 -1
  17. package/dist/component/public/enterprise/scim.d.ts.map +1 -1
  18. package/dist/component/public/enterprise/scim.js.map +1 -1
  19. package/dist/component/public/enterprise/secrets.d.ts.map +1 -1
  20. package/dist/component/public/enterprise/secrets.js.map +1 -1
  21. package/dist/component/public/enterprise/webhooks.d.ts.map +1 -1
  22. package/dist/component/public/enterprise/webhooks.js.map +1 -1
  23. package/dist/component/public/factors/devices.d.ts.map +1 -1
  24. package/dist/component/public/factors/devices.js.map +1 -1
  25. package/dist/component/public/factors/passkeys.d.ts.map +1 -1
  26. package/dist/component/public/factors/passkeys.js.map +1 -1
  27. package/dist/component/public/factors/totp.d.ts.map +1 -1
  28. package/dist/component/public/factors/totp.js.map +1 -1
  29. package/dist/component/public/groups/core.js.map +1 -1
  30. package/dist/component/public/groups/invites.d.ts.map +1 -1
  31. package/dist/component/public/groups/invites.js.map +1 -1
  32. package/dist/component/public/groups/members.d.ts.map +1 -1
  33. package/dist/component/public/groups/members.js.map +1 -1
  34. package/dist/component/public/identity/accounts.d.ts.map +1 -1
  35. package/dist/component/public/identity/accounts.js.map +1 -1
  36. package/dist/component/public/identity/codes.d.ts.map +1 -1
  37. package/dist/component/public/identity/codes.js.map +1 -1
  38. package/dist/component/public/identity/sessions.d.ts.map +1 -1
  39. package/dist/component/public/identity/sessions.js.map +1 -1
  40. package/dist/component/public/identity/tokens.d.ts.map +1 -1
  41. package/dist/component/public/identity/tokens.js.map +1 -1
  42. package/dist/component/public/identity/users.d.ts.map +1 -1
  43. package/dist/component/public/identity/users.js.map +1 -1
  44. package/dist/component/public/identity/verifiers.d.ts.map +1 -1
  45. package/dist/component/public/identity/verifiers.js.map +1 -1
  46. package/dist/component/public/security/keys.d.ts.map +1 -1
  47. package/dist/component/public/security/keys.js.map +1 -1
  48. package/dist/component/public/security/limits.d.ts.map +1 -1
  49. package/dist/component/public/security/limits.js.map +1 -1
  50. package/dist/component/schema.d.ts +42 -42
  51. package/dist/component/server/auth.d.ts +37 -40
  52. package/dist/component/server/auth.d.ts.map +1 -1
  53. package/dist/component/server/auth.js +57 -23
  54. package/dist/component/server/auth.js.map +1 -1
  55. package/dist/component/server/core.js +116 -235
  56. package/dist/component/server/core.js.map +1 -1
  57. package/dist/component/server/crypto.js +25 -7
  58. package/dist/component/server/crypto.js.map +1 -1
  59. package/dist/component/server/device.js +58 -15
  60. package/dist/component/server/device.js.map +1 -1
  61. package/dist/component/server/enterprise/domain.js +148 -59
  62. package/dist/component/server/enterprise/domain.js.map +1 -1
  63. package/dist/component/server/enterprise/http.js +36 -15
  64. package/dist/component/server/enterprise/http.js.map +1 -1
  65. package/dist/component/server/enterprise/oidc.js +1 -1
  66. package/dist/component/server/http.js +26 -21
  67. package/dist/component/server/http.js.map +1 -1
  68. package/dist/component/server/identity.js +5 -2
  69. package/dist/component/server/identity.js.map +1 -1
  70. package/dist/component/server/limits.js +21 -30
  71. package/dist/component/server/limits.js.map +1 -1
  72. package/dist/component/server/mutations/account.js +12 -10
  73. package/dist/component/server/mutations/account.js.map +1 -1
  74. package/dist/component/server/mutations/code.js +5 -2
  75. package/dist/component/server/mutations/code.js.map +1 -1
  76. package/dist/component/server/mutations/invalidate.js +1 -1
  77. package/dist/component/server/mutations/invalidate.js.map +1 -1
  78. package/dist/component/server/mutations/oauth.js +10 -4
  79. package/dist/component/server/mutations/oauth.js.map +1 -1
  80. package/dist/component/server/mutations/refresh.js +2 -2
  81. package/dist/component/server/mutations/refresh.js.map +1 -1
  82. package/dist/component/server/mutations/register.js +46 -42
  83. package/dist/component/server/mutations/register.js.map +1 -1
  84. package/dist/component/server/mutations/retrieve.js +21 -25
  85. package/dist/component/server/mutations/retrieve.js.map +1 -1
  86. package/dist/component/server/mutations/signature.js +10 -4
  87. package/dist/component/server/mutations/signature.js.map +1 -1
  88. package/dist/component/server/mutations/signout.js.map +1 -1
  89. package/dist/component/server/mutations/store.js +9 -24
  90. package/dist/component/server/mutations/store.js.map +1 -1
  91. package/dist/component/server/mutations/verifier.js.map +1 -1
  92. package/dist/component/server/mutations/verify.js +1 -1
  93. package/dist/component/server/mutations/verify.js.map +1 -1
  94. package/dist/component/server/oauth.js +53 -16
  95. package/dist/component/server/oauth.js.map +1 -1
  96. package/dist/component/server/passkey.js +115 -31
  97. package/dist/component/server/passkey.js.map +1 -1
  98. package/dist/component/server/redirects.js +9 -3
  99. package/dist/component/server/redirects.js.map +1 -1
  100. package/dist/component/server/refresh.js +10 -7
  101. package/dist/component/server/refresh.js.map +1 -1
  102. package/dist/component/server/runtime.d.ts +1 -1
  103. package/dist/component/server/runtime.d.ts.map +1 -1
  104. package/dist/component/server/runtime.js +62 -20
  105. package/dist/component/server/runtime.js.map +1 -1
  106. package/dist/component/server/signin.js +34 -10
  107. package/dist/component/server/signin.js.map +1 -1
  108. package/dist/component/server/totp.js +79 -19
  109. package/dist/component/server/totp.js.map +1 -1
  110. package/dist/component/server/types.d.ts +12 -20
  111. package/dist/component/server/types.d.ts.map +1 -1
  112. package/dist/component/server/types.js.map +1 -1
  113. package/dist/component/server/users.js +6 -3
  114. package/dist/component/server/users.js.map +1 -1
  115. package/dist/component/server/utils.js +10 -4
  116. package/dist/component/server/utils.js.map +1 -1
  117. package/dist/core/types.d.ts +14 -22
  118. package/dist/core/types.d.ts.map +1 -1
  119. package/dist/factors/device.js +8 -9
  120. package/dist/factors/device.js.map +1 -1
  121. package/dist/factors/passkey.js +18 -21
  122. package/dist/factors/passkey.js.map +1 -1
  123. package/dist/providers/password.js +66 -81
  124. package/dist/providers/password.js.map +1 -1
  125. package/dist/runtime/invite.js +2 -8
  126. package/dist/runtime/invite.js.map +1 -1
  127. package/dist/server/auth.d.ts +37 -40
  128. package/dist/server/auth.d.ts.map +1 -1
  129. package/dist/server/auth.js +57 -23
  130. package/dist/server/auth.js.map +1 -1
  131. package/dist/server/core.d.ts +71 -159
  132. package/dist/server/core.d.ts.map +1 -1
  133. package/dist/server/core.js +116 -235
  134. package/dist/server/core.js.map +1 -1
  135. package/dist/server/crypto.d.ts.map +1 -1
  136. package/dist/server/crypto.js +25 -7
  137. package/dist/server/crypto.js.map +1 -1
  138. package/dist/server/device.js +58 -15
  139. package/dist/server/device.js.map +1 -1
  140. package/dist/server/enterprise/domain.d.ts +0 -8
  141. package/dist/server/enterprise/domain.d.ts.map +1 -1
  142. package/dist/server/enterprise/domain.js +148 -59
  143. package/dist/server/enterprise/domain.js.map +1 -1
  144. package/dist/server/enterprise/http.d.ts.map +1 -1
  145. package/dist/server/enterprise/http.js +35 -14
  146. package/dist/server/enterprise/http.js.map +1 -1
  147. package/dist/server/http.d.ts +2 -2
  148. package/dist/server/http.d.ts.map +1 -1
  149. package/dist/server/http.js +25 -20
  150. package/dist/server/http.js.map +1 -1
  151. package/dist/server/identity.js +5 -2
  152. package/dist/server/identity.js.map +1 -1
  153. package/dist/server/index.d.ts +2 -2
  154. package/dist/server/limits.js +21 -30
  155. package/dist/server/limits.js.map +1 -1
  156. package/dist/server/mounts.d.ts +24 -62
  157. package/dist/server/mounts.d.ts.map +1 -1
  158. package/dist/server/mounts.js +45 -106
  159. package/dist/server/mounts.js.map +1 -1
  160. package/dist/server/mutations/account.d.ts +8 -9
  161. package/dist/server/mutations/account.d.ts.map +1 -1
  162. package/dist/server/mutations/account.js +11 -9
  163. package/dist/server/mutations/account.js.map +1 -1
  164. package/dist/server/mutations/code.d.ts +12 -12
  165. package/dist/server/mutations/code.d.ts.map +1 -1
  166. package/dist/server/mutations/code.js +5 -2
  167. package/dist/server/mutations/code.js.map +1 -1
  168. package/dist/server/mutations/invalidate.d.ts +4 -4
  169. package/dist/server/mutations/invalidate.d.ts.map +1 -1
  170. package/dist/server/mutations/invalidate.js.map +1 -1
  171. package/dist/server/mutations/oauth.d.ts +14 -12
  172. package/dist/server/mutations/oauth.d.ts.map +1 -1
  173. package/dist/server/mutations/oauth.js +9 -3
  174. package/dist/server/mutations/oauth.js.map +1 -1
  175. package/dist/server/mutations/refresh.d.ts +3 -3
  176. package/dist/server/mutations/refresh.d.ts.map +1 -1
  177. package/dist/server/mutations/refresh.js +1 -1
  178. package/dist/server/mutations/refresh.js.map +1 -1
  179. package/dist/server/mutations/register.d.ts +11 -11
  180. package/dist/server/mutations/register.d.ts.map +1 -1
  181. package/dist/server/mutations/register.js +45 -41
  182. package/dist/server/mutations/register.js.map +1 -1
  183. package/dist/server/mutations/retrieve.d.ts +6 -6
  184. package/dist/server/mutations/retrieve.d.ts.map +1 -1
  185. package/dist/server/mutations/retrieve.js +20 -24
  186. package/dist/server/mutations/retrieve.js.map +1 -1
  187. package/dist/server/mutations/signature.d.ts +6 -7
  188. package/dist/server/mutations/signature.d.ts.map +1 -1
  189. package/dist/server/mutations/signature.js +9 -3
  190. package/dist/server/mutations/signature.js.map +1 -1
  191. package/dist/server/mutations/signin.d.ts +5 -5
  192. package/dist/server/mutations/signin.d.ts.map +1 -1
  193. package/dist/server/mutations/signout.js.map +1 -1
  194. package/dist/server/mutations/store.d.ts +83 -83
  195. package/dist/server/mutations/store.js +8 -23
  196. package/dist/server/mutations/store.js.map +1 -1
  197. package/dist/server/mutations/verifier.js.map +1 -1
  198. package/dist/server/mutations/verify.d.ts +7 -7
  199. package/dist/server/mutations/verify.d.ts.map +1 -1
  200. package/dist/server/mutations/verify.js.map +1 -1
  201. package/dist/server/oauth.js +53 -16
  202. package/dist/server/oauth.js.map +1 -1
  203. package/dist/server/passkey.d.ts +2 -2
  204. package/dist/server/passkey.d.ts.map +1 -1
  205. package/dist/server/passkey.js +114 -30
  206. package/dist/server/passkey.js.map +1 -1
  207. package/dist/server/redirects.js +9 -3
  208. package/dist/server/redirects.js.map +1 -1
  209. package/dist/server/refresh.js +10 -7
  210. package/dist/server/refresh.js.map +1 -1
  211. package/dist/server/runtime.d.ts +7 -7
  212. package/dist/server/runtime.d.ts.map +1 -1
  213. package/dist/server/runtime.js +61 -19
  214. package/dist/server/runtime.js.map +1 -1
  215. package/dist/server/signin.js +34 -10
  216. package/dist/server/signin.js.map +1 -1
  217. package/dist/server/ssr.d.ts.map +1 -1
  218. package/dist/server/ssr.js +175 -184
  219. package/dist/server/ssr.js.map +1 -1
  220. package/dist/server/totp.js +78 -18
  221. package/dist/server/totp.js.map +1 -1
  222. package/dist/server/types.d.ts +13 -21
  223. package/dist/server/types.d.ts.map +1 -1
  224. package/dist/server/types.js.map +1 -1
  225. package/dist/server/users.js +6 -3
  226. package/dist/server/users.js.map +1 -1
  227. package/dist/server/utils.js +10 -4
  228. package/dist/server/utils.js.map +1 -1
  229. package/package.json +1 -5
  230. package/src/authorization/index.ts +1 -1
  231. package/src/client/core/types.ts +14 -14
  232. package/src/client/factors/device.ts +10 -12
  233. package/src/client/factors/passkey.ts +23 -26
  234. package/src/client/index.ts +54 -64
  235. package/src/client/runtime/invite.ts +5 -7
  236. package/src/component/index.ts +1 -1
  237. package/src/component/public/enterprise/audit.ts +6 -1
  238. package/src/component/public/enterprise/core.ts +1 -0
  239. package/src/component/public/enterprise/domains.ts +5 -1
  240. package/src/component/public/enterprise/scim.ts +1 -0
  241. package/src/component/public/enterprise/secrets.ts +1 -0
  242. package/src/component/public/enterprise/webhooks.ts +1 -0
  243. package/src/component/public/factors/devices.ts +1 -0
  244. package/src/component/public/factors/passkeys.ts +1 -0
  245. package/src/component/public/factors/totp.ts +1 -0
  246. package/src/component/public/groups/core.ts +1 -1
  247. package/src/component/public/groups/invites.ts +7 -1
  248. package/src/component/public/groups/members.ts +1 -0
  249. package/src/component/public/identity/accounts.ts +1 -0
  250. package/src/component/public/identity/codes.ts +1 -0
  251. package/src/component/public/identity/sessions.ts +1 -0
  252. package/src/component/public/identity/tokens.ts +1 -0
  253. package/src/component/public/identity/users.ts +1 -0
  254. package/src/component/public/identity/verifiers.ts +1 -0
  255. package/src/component/public/security/keys.ts +1 -0
  256. package/src/component/public/security/limits.ts +1 -0
  257. package/src/providers/password.ts +89 -110
  258. package/src/server/auth.ts +92 -70
  259. package/src/server/core.ts +197 -233
  260. package/src/server/crypto.ts +31 -29
  261. package/src/server/device.ts +65 -32
  262. package/src/server/enterprise/domain.ts +158 -170
  263. package/src/server/enterprise/http.ts +46 -39
  264. package/src/server/http.ts +36 -30
  265. package/src/server/identity.ts +5 -5
  266. package/src/server/index.ts +1 -1
  267. package/src/server/limits.ts +53 -80
  268. package/src/server/mounts.ts +47 -74
  269. package/src/server/mutations/account.ts +22 -36
  270. package/src/server/mutations/code.ts +6 -6
  271. package/src/server/mutations/invalidate.ts +1 -1
  272. package/src/server/mutations/oauth.ts +14 -8
  273. package/src/server/mutations/refresh.ts +5 -4
  274. package/src/server/mutations/register.ts +87 -132
  275. package/src/server/mutations/retrieve.ts +44 -44
  276. package/src/server/mutations/signature.ts +13 -6
  277. package/src/server/mutations/signout.ts +1 -1
  278. package/src/server/mutations/store.ts +16 -31
  279. package/src/server/mutations/verifier.ts +1 -1
  280. package/src/server/mutations/verify.ts +3 -5
  281. package/src/server/oauth.ts +60 -69
  282. package/src/server/passkey.ts +567 -517
  283. package/src/server/redirects.ts +10 -6
  284. package/src/server/refresh.ts +14 -18
  285. package/src/server/runtime.ts +70 -55
  286. package/src/server/signin.ts +44 -37
  287. package/src/server/ssr.ts +390 -407
  288. package/src/server/totp.ts +85 -35
  289. package/src/server/types.ts +19 -22
  290. package/src/server/users.ts +7 -6
  291. package/src/server/utils.ts +10 -12
  292. package/dist/component/server/authError.js +0 -34
  293. package/dist/component/server/authError.js.map +0 -1
  294. package/dist/component/server/errors.d.ts +0 -1
  295. package/dist/component/server/errors.js +0 -137
  296. package/dist/component/server/errors.js.map +0 -1
  297. package/dist/server/authError.d.ts +0 -46
  298. package/dist/server/authError.d.ts.map +0 -1
  299. package/dist/server/authError.js +0 -34
  300. package/dist/server/authError.js.map +0 -1
  301. package/dist/server/errors.d.ts +0 -177
  302. package/dist/server/errors.d.ts.map +0 -1
  303. package/dist/server/errors.js +0 -212
  304. package/dist/server/errors.js.map +0 -1
  305. package/src/server/authError.ts +0 -44
  306. package/src/server/errors.ts +0 -290
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","names":[],"sources":["../../../../src/component/public/groups/core.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport type { Id } from \"../../_generated/dataModel\";\nimport { mutation, query } from \"../../functions\";\nimport { vGroupDoc, vPaginated, vTag } from \"../../model\";\n\n\ntype TagPair = { key: string; value: string };\n\nfunction normalizeTag(tag: TagPair): TagPair {\n return {\n key: tag.key.trim().toLowerCase(),\n value: tag.value.trim().toLowerCase(),\n };\n}\n\nfunction normalizeTags(tags: TagPair[]): TagPair[] {\n const seen = new Set<string>();\n const result: TagPair[] = [];\n for (const raw of tags) {\n const t = normalizeTag(raw);\n const composite = `${t.key}\\0${t.value}`;\n if (!seen.has(composite)) {\n seen.add(composite);\n result.push(t);\n }\n }\n return result;\n}\n/**\n * Create a new group. Groups are hierarchical — set `parentGroupId` to nest\n * under an existing group, or omit it to create a root-level group.\n *\n * Root groups self-reference their own ID as `rootGroupId`. Child groups\n * inherit `rootGroupId` from their parent chain. Tags are normalized\n * (trimmed and lowercased) and deduplicated before storage, and companion\n * `GroupTag` rows are created for indexed lookups.\n *\n * @param args.name - The display name for the group.\n * @param args.slug - An optional URL-friendly identifier for the group (e.g. `\"engineering\"`).\n * @param args.type - An optional application-defined group type (e.g. `\"organization\"`, `\"team\"`).\n * @param args.parentGroupId - The ID of an existing group to nest under. Omit to create a root-level group.\n * @param args.tags - An optional array of `{ key, value }` tag pairs to attach to the group for filtering.\n * @param args.extend - An optional arbitrary payload for application-specific metadata.\n * @returns The `Id<\"Group\">` of the newly created group document.\n *\n * @example\n * ```ts\n * const groupId = await ctx.runMutation(components.auth.groups.groupCreate, {\n * name: \"Acme Corp\",\n * slug: \"acme-corp\",\n * type: \"organization\",\n * tags: [{ key: \"plan\", value: \"enterprise\" }],\n * });\n * ```\n */\nexport const groupCreate = mutation({\n args: {\n name: v.string(),\n slug: v.optional(v.string()),\n type: v.optional(v.string()),\n parentGroupId: v.optional(v.id(\"Group\")),\n tags: v.optional(v.array(vTag)),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"Group\"),\n handler: async (ctx, args) => {\n const { tags: rawTags, ...rest } = args;\n const normalizedTags = rawTags ? normalizeTags(rawTags) : undefined;\n const isRoot = !args.parentGroupId;\n // Compute rootGroupId: root groups self-reference, children inherit from parent\n let rootGroupId: Id<\"Group\"> | undefined;\n if (!isRoot && args.parentGroupId) {\n const parent = await ctx.db.get(args.parentGroupId);\n rootGroupId = parent?.rootGroupId ?? args.parentGroupId;\n }\n const groupId = await ctx.db.insert(\"Group\", {\n ...rest,\n tags: normalizedTags,\n isRoot,\n rootGroupId: isRoot ? undefined : rootGroupId,\n });\n // Self-reference for root groups (need the ID after insert)\n if (isRoot) {\n await ctx.db.patch(groupId, { rootGroupId: groupId });\n }\n // Sync companion group_tag rows\n if (normalizedTags) {\n for (const tag of normalizedTags) {\n await ctx.db.insert(\"GroupTag\", {\n group_id: groupId,\n key: tag.key,\n value: tag.value,\n });\n }\n }\n return groupId;\n },\n});\n\n/**\n * Retrieve a group by its document ID.\n *\n * Performs a direct lookup in the `Group` table and returns the full group\n * document, or `null` if no group exists with the given ID.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to retrieve.\n * @returns The group document (including `name`, `slug`, `type`, `tags`, hierarchy fields, etc.) or `null` if not found.\n *\n * @example\n * ```ts\n * const group = await ctx.runQuery(components.auth.groups.groupGet, {\n * groupId: existingGroupId,\n * });\n * if (group !== null) {\n * console.log(group.name, group.slug);\n * }\n * ```\n */\nexport const groupGet = query({\n args: { groupId: v.id(\"Group\") },\n returns: v.union(vGroupDoc, v.null()),\n handler: async (ctx, { groupId }) => {\n return await ctx.db.get(\"Group\", groupId);\n },\n});\n\n/**\n * List groups with optional filtering, sorting, and pagination.\n *\n * Returns `{ items, nextCursor }`. Empty `where` returns **all** groups.\n * The query engine selects the best database index based on the combination\n * of filter fields provided. Tag filters (`tagsAll`, `tagsAny`) are resolved\n * via the `GroupTag` companion table and intersected/unioned with index results.\n *\n * @param args.where - Optional filter criteria for narrowing results.\n * @param args.where.slug - Match groups with this exact slug.\n * @param args.where.type - Match groups with this exact type.\n * @param args.where.parentGroupId - Match groups that are direct children of the specified parent group.\n * @param args.where.name - Match groups with this exact name.\n * @param args.where.isRoot - When `true`, return only root-level groups; when `false`, only child groups.\n * @param args.where.tagsAll - An array of `{ key, value }` pairs; only groups that have **all** of these tags are returned.\n * @param args.where.tagsAny - An array of `{ key, value }` pairs; groups that have **at least one** of these tags are returned.\n * @param args.limit - Maximum number of items per page (clamped to 1..100, defaults to 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor` to fetch the next page, or `null` to start from the beginning.\n * @param args.orderBy - The field to sort by: `\"_creationTime\"`, `\"name\"`, `\"slug\"`, or `\"type\"`.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (defaults to `\"desc\"`).\n * @returns An object `{ items, nextCursor }` where `items` is an array of group documents and `nextCursor` is `null` when there are no more pages.\n *\n * @example\n * ```ts\n * const { items, nextCursor } = await ctx.runQuery(\n * components.auth.groups.groupList,\n * {\n * where: { type: \"team\", isRoot: false },\n * limit: 20,\n * order: \"asc\",\n * },\n * );\n * ```\n */\nexport const groupList = query({\n args: {\n where: v.optional(\n v.object({\n slug: v.optional(v.string()),\n type: v.optional(v.string()),\n parentGroupId: v.optional(v.id(\"Group\")),\n name: v.optional(v.string()),\n isRoot: v.optional(v.boolean()),\n tagsAll: v.optional(v.array(vTag)),\n tagsAny: v.optional(v.array(vTag)),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n orderBy: v.optional(\n v.union(\n v.literal(\"_creationTime\"),\n v.literal(\"name\"),\n v.literal(\"slug\"),\n v.literal(\"type\"),\n ),\n ),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vGroupDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n // ---- Resolve tag filters into a Set<Id<\"Group\">> ----\n let tagFilteredIds: Set<string> | null = null;\n\n if (where.tagsAll && where.tagsAll.length > 0) {\n // Intersect: group must have ALL specified tags\n let allSet: Set<string> | null = null;\n for (const rawTag of where.tagsAll) {\n const t = normalizeTag(rawTag);\n const rows = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_key_value\", (idx) =>\n idx.eq(\"key\", t.key).eq(\"value\", t.value),\n )\n .collect();\n const ids = new Set(rows.map((r) => r.group_id as string));\n if (allSet === null) {\n allSet = ids;\n } else {\n // Intersect\n for (const id of allSet) {\n if (!ids.has(id)) allSet.delete(id);\n }\n }\n // Short-circuit: empty intersection\n if (allSet.size === 0) break;\n }\n tagFilteredIds = allSet ?? new Set();\n }\n\n if (where.tagsAny && where.tagsAny.length > 0) {\n // Union: group must have at least one of the specified tags\n const anySet = new Set<string>();\n for (const rawTag of where.tagsAny) {\n const t = normalizeTag(rawTag);\n const rows = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_key_value\", (idx) =>\n idx.eq(\"key\", t.key).eq(\"value\", t.value),\n )\n .collect();\n for (const r of rows) {\n anySet.add(r.group_id as string);\n }\n }\n if (tagFilteredIds !== null) {\n // AND with tagsAll result\n for (const id of tagFilteredIds) {\n if (!anySet.has(id)) tagFilteredIds.delete(id);\n }\n } else {\n tagFilteredIds = anySet;\n }\n }\n\n // ---- Pick best index based on non-tag where fields ----\n let q;\n if (where.type !== undefined && where.parentGroupId !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"type_parent_group_id\", (idx) =>\n idx.eq(\"type\", where.type!).eq(\"parentGroupId\", where.parentGroupId!),\n );\n } else if (where.slug !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"slug\", (idx) => idx.eq(\"slug\", where.slug!));\n } else if (where.type !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"type\", (idx) => idx.eq(\"type\", where.type!));\n } else if (where.parentGroupId !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"parent_group_id\", (idx) =>\n idx.eq(\"parentGroupId\", where.parentGroupId!),\n );\n } else if (where.isRoot !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"is_root\", (idx) => idx.eq(\"isRoot\", where.isRoot!));\n } else {\n q = ctx.db.query(\"Group\");\n }\n\n // Apply remaining non-tag filters not covered by index\n if (where.name !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"name\"), where.name!));\n }\n // isRoot filter when not already used as primary index\n if (\n where.isRoot !== undefined &&\n where.parentGroupId === undefined &&\n (where.type !== undefined || where.slug !== undefined)\n ) {\n q = q.filter((f) => f.eq(f.field(\"isRoot\"), where.isRoot!));\n }\n // slug filter when not used as index\n if (where.slug !== undefined && where.type !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"slug\"), where.slug!));\n }\n\n q = q.order(order);\n\n let all = await q.collect();\n\n // Apply tag filter (intersect with resolved groupIds)\n if (tagFilteredIds !== null) {\n all = all.filter((doc) => tagFilteredIds!.has(doc._id as string));\n }\n\n // Cursor-based pagination\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Update a group's mutable fields such as `name`, `slug`, `tags`, `extend`,\n * and `parentGroupId`.\n *\n * When `parentGroupId` is changed the mutation automatically recomputes\n * `isRoot` and `rootGroupId` for the target group **and** cascades the new\n * `rootGroupId` to all descendant groups. When `tags` are provided they are\n * normalized, deduplicated, and the companion `GroupTag` rows are fully\n * replaced (delete-then-insert).\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to update.\n * @param args.data - A partial object of fields to patch. Supported keys include `name`, `slug`, `type`, `parentGroupId`, `tags`, and `extend`.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.groupUpdate, {\n * groupId: existingGroupId,\n * data: {\n * name: \"Acme Corp (renamed)\",\n * tags: [{ key: \"plan\", value: \"pro\" }],\n * },\n * });\n * ```\n */\nexport const groupUpdate = mutation({\n args: { groupId: v.id(\"Group\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { groupId, data }) => {\n // If parentGroupId is changing, recompute rootGroupId + isRoot for this group and descendants\n if (data.parentGroupId !== undefined) {\n const oldGroup = await ctx.db.get(\"Group\", groupId);\n const oldRootGroupId = oldGroup?.rootGroupId;\n const newParentGroupId = data.parentGroupId as Id<\"Group\"> | undefined;\n const newIsRoot = !newParentGroupId;\n let newRootGroupId: Id<\"Group\">;\n if (newIsRoot) {\n newRootGroupId = groupId;\n } else {\n const parent = await ctx.db.get(\"Group\", newParentGroupId!);\n newRootGroupId = parent?.rootGroupId ?? newParentGroupId!;\n }\n data.isRoot = newIsRoot;\n data.rootGroupId = newRootGroupId;\n // Cascade to descendants if rootGroupId changed\n if (oldRootGroupId && oldRootGroupId !== newRootGroupId) {\n const descendants = await ctx.db\n .query(\"Group\")\n .withIndex(\"root_group_id\", (q) =>\n q.eq(\"rootGroupId\", oldRootGroupId),\n )\n .collect();\n for (const desc of descendants) {\n if (desc._id !== groupId) {\n await ctx.db.patch(\"Group\", desc._id, {\n rootGroupId: newRootGroupId,\n });\n }\n }\n }\n }\n // If tags are being updated, normalize and replace the full tag set\n if (data.tags !== undefined) {\n const normalizedTags: TagPair[] = Array.isArray(data.tags)\n ? normalizeTags(data.tags as TagPair[])\n : [];\n // Delete existing group_tag rows for this group\n const existingTags = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_group\", (idx) => idx.eq(\"group_id\", groupId))\n .collect();\n for (const existing of existingTags) {\n await ctx.db.delete(\"GroupTag\", existing._id);\n }\n // Insert new normalized group_tag rows\n for (const tag of normalizedTags) {\n await ctx.db.insert(\"GroupTag\", {\n group_id: groupId,\n key: tag.key,\n value: tag.value,\n });\n }\n // Patch group with normalized tags (empty array = clear all)\n await ctx.db.patch(\"Group\", groupId, {\n ...data,\n tags: normalizedTags.length > 0 ? normalizedTags : undefined,\n });\n } else {\n await ctx.db.patch(\"Group\", groupId, data);\n }\n return null;\n },\n});\n\n/**\n * Delete a group and all of its descendants. This cascades to:\n * - All child groups (recursively)\n * - All members of this group and its descendants\n * - All invites for this group and its descendants\n * - All companion `GroupTag` rows for this group and its descendants\n *\n * The deletion walks the group tree depth-first, removing leaves before\n * parents, so referential integrity is maintained throughout.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to delete. All children are deleted recursively.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // Delete an organization and everything nested under it\n * await ctx.runMutation(components.auth.groups.groupDelete, {\n * groupId: organizationGroupId,\n * });\n * ```\n */\nexport const groupDelete = mutation({\n args: { groupId: v.id(\"Group\") },\n returns: v.null(),\n handler: async (ctx, { groupId }) => {\n const deleteGroup = async (id: typeof groupId) => {\n const children = await ctx.db\n .query(\"Group\")\n .withIndex(\"parent_group_id\", (q) => q.eq(\"parentGroupId\", id))\n .collect();\n for (const child of children) {\n await deleteGroup(child._id);\n }\n\n const members = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id\", (q) => q.eq(\"groupId\", id))\n .collect();\n for (const member of members) {\n await ctx.db.delete(\"GroupMember\", member._id);\n }\n\n const invites = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id\", (q) => q.eq(\"groupId\", id))\n .collect();\n for (const invite of invites) {\n await ctx.db.delete(\"GroupInvite\", invite._id);\n }\n\n // Delete companion group_tag rows\n const tags = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_group\", (q) => q.eq(\"group_id\", id))\n .collect();\n for (const tag of tags) {\n await ctx.db.delete(\"GroupTag\", tag._id);\n }\n\n await ctx.db.delete(\"Group\", id);\n };\n\n await deleteGroup(groupId);\n return null;\n },\n});\n\n// ============================================================================\n// Members\n// ============================================================================\n"],"mappings":";;;;;AAQA,SAAS,aAAa,KAAuB;AAC3C,QAAO;EACL,KAAK,IAAI,IAAI,MAAM,CAAC,aAAa;EACjC,OAAO,IAAI,MAAM,MAAM,CAAC,aAAa;EACtC;;AAGH,SAAS,cAAc,MAA4B;CACjD,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,SAAoB,EAAE;AAC5B,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,IAAI,aAAa,IAAI;EAC3B,MAAM,YAAY,GAAG,EAAE,IAAI,IAAI,EAAE;AACjC,MAAI,CAAC,KAAK,IAAI,UAAU,EAAE;AACxB,QAAK,IAAI,UAAU;AACnB,UAAO,KAAK,EAAE;;;AAGlB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BT,MAAa,cAAc,SAAS;CAClC,MAAM;EACJ,MAAM,EAAE,QAAQ;EAChB,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC5B,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC5B,eAAe,EAAE,SAAS,EAAE,GAAG,QAAQ,CAAC;EACxC,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;EAC/B,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC;EAC5B;CACD,SAAS,EAAE,GAAG,QAAQ;CACtB,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,EAAE,MAAM,SAAS,GAAG,SAAS;EACnC,MAAM,iBAAiB,UAAU,cAAc,QAAQ,GAAG;EAC1D,MAAM,SAAS,CAAC,KAAK;EAErB,IAAI;AACJ,MAAI,CAAC,UAAU,KAAK,cAElB,gBADe,MAAM,IAAI,GAAG,IAAI,KAAK,cAAc,GAC7B,eAAe,KAAK;EAE5C,MAAM,UAAU,MAAM,IAAI,GAAG,OAAO,SAAS;GAC3C,GAAG;GACH,MAAM;GACN;GACA,aAAa,SAAS,SAAY;GACnC,CAAC;AAEF,MAAI,OACF,OAAM,IAAI,GAAG,MAAM,SAAS,EAAE,aAAa,SAAS,CAAC;AAGvD,MAAI,eACF,MAAK,MAAM,OAAO,eAChB,OAAM,IAAI,GAAG,OAAO,YAAY;GAC9B,UAAU;GACV,KAAK,IAAI;GACT,OAAO,IAAI;GACZ,CAAC;AAGN,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,WAAW,MAAM;CAC5B,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE;CAChC,SAAS,EAAE,MAAM,WAAW,EAAE,MAAM,CAAC;CACrC,SAAS,OAAO,KAAK,EAAE,cAAc;AACnC,SAAO,MAAM,IAAI,GAAG,IAAI,SAAS,QAAQ;;CAE5C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCF,MAAa,YAAY,MAAM;CAC7B,MAAM;EACJ,OAAO,EAAE,SACP,EAAE,OAAO;GACP,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;GAC5B,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;GAC5B,eAAe,EAAE,SAAS,EAAE,GAAG,QAAQ,CAAC;GACxC,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;GAC5B,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC;GAC/B,SAAS,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;GAClC,SAAS,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;GACnC,CAAC,CACH;EACD,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC7B,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC;EACjD,SAAS,EAAE,SACT,EAAE,MACA,EAAE,QAAQ,gBAAgB,EAC1B,EAAE,QAAQ,OAAO,EACjB,EAAE,QAAQ,OAAO,EACjB,EAAE,QAAQ,OAAO,CAClB,CACF;EACD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,MAAM,EAAE,EAAE,QAAQ,OAAO,CAAC,CAAC;EAChE;CACD,SAAS,WAAW,UAAU;CAC9B,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,QAAQ,KAAK,SAAS,EAAE;EAC9B,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI,EAAE,EAAE,IAAI;EAC1D,MAAM,QAAQ,KAAK,SAAS;EAG5B,IAAI,iBAAqC;AAEzC,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;GAE7C,IAAI,SAA6B;AACjC,QAAK,MAAM,UAAU,MAAM,SAAS;IAClC,MAAM,IAAI,aAAa,OAAO;IAC9B,MAAM,OAAO,MAAM,IAAI,GACpB,MAAM,WAAW,CACjB,UAAU,iBAAiB,QAC1B,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC,GAAG,SAAS,EAAE,MAAM,CAC1C,CACA,SAAS;IACZ,MAAM,MAAM,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,SAAmB,CAAC;AAC1D,QAAI,WAAW,KACb,UAAS;QAGT,MAAK,MAAM,MAAM,OACf,KAAI,CAAC,IAAI,IAAI,GAAG,CAAE,QAAO,OAAO,GAAG;AAIvC,QAAI,OAAO,SAAS,EAAG;;AAEzB,oBAAiB,0BAAU,IAAI,KAAK;;AAGtC,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;GAE7C,MAAM,yBAAS,IAAI,KAAa;AAChC,QAAK,MAAM,UAAU,MAAM,SAAS;IAClC,MAAM,IAAI,aAAa,OAAO;IAC9B,MAAM,OAAO,MAAM,IAAI,GACpB,MAAM,WAAW,CACjB,UAAU,iBAAiB,QAC1B,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC,GAAG,SAAS,EAAE,MAAM,CAC1C,CACA,SAAS;AACZ,SAAK,MAAM,KAAK,KACd,QAAO,IAAI,EAAE,SAAmB;;AAGpC,OAAI,mBAAmB,MAErB;SAAK,MAAM,MAAM,eACf,KAAI,CAAC,OAAO,IAAI,GAAG,CAAE,gBAAe,OAAO,GAAG;SAGhD,kBAAiB;;EAKrB,IAAI;AACJ,MAAI,MAAM,SAAS,UAAa,MAAM,kBAAkB,OACtD,KAAI,IAAI,GACL,MAAM,QAAQ,CACd,UAAU,yBAAyB,QAClC,IAAI,GAAG,QAAQ,MAAM,KAAM,CAAC,GAAG,iBAAiB,MAAM,cAAe,CACtE;WACM,MAAM,SAAS,OACxB,KAAI,IAAI,GACL,MAAM,QAAQ,CACd,UAAU,SAAS,QAAQ,IAAI,GAAG,QAAQ,MAAM,KAAM,CAAC;WACjD,MAAM,SAAS,OACxB,KAAI,IAAI,GACL,MAAM,QAAQ,CACd,UAAU,SAAS,QAAQ,IAAI,GAAG,QAAQ,MAAM,KAAM,CAAC;WACjD,MAAM,kBAAkB,OACjC,KAAI,IAAI,GACL,MAAM,QAAQ,CACd,UAAU,oBAAoB,QAC7B,IAAI,GAAG,iBAAiB,MAAM,cAAe,CAC9C;WACM,MAAM,WAAW,OAC1B,KAAI,IAAI,GACL,MAAM,QAAQ,CACd,UAAU,YAAY,QAAQ,IAAI,GAAG,UAAU,MAAM,OAAQ,CAAC;MAEjE,KAAI,IAAI,GAAG,MAAM,QAAQ;AAI3B,MAAI,MAAM,SAAS,OACjB,KAAI,EAAE,QAAQ,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,MAAM,KAAM,CAAC;AAGzD,MACE,MAAM,WAAW,UACjB,MAAM,kBAAkB,WACvB,MAAM,SAAS,UAAa,MAAM,SAAS,QAE5C,KAAI,EAAE,QAAQ,MAAM,EAAE,GAAG,EAAE,MAAM,SAAS,EAAE,MAAM,OAAQ,CAAC;AAG7D,MAAI,MAAM,SAAS,UAAa,MAAM,SAAS,OAC7C,KAAI,EAAE,QAAQ,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,MAAM,KAAM,CAAC;AAGzD,MAAI,EAAE,MAAM,MAAM;EAElB,IAAI,MAAM,MAAM,EAAE,SAAS;AAG3B,MAAI,mBAAmB,KACrB,OAAM,IAAI,QAAQ,QAAQ,eAAgB,IAAI,IAAI,IAAc,CAAC;EAInE,IAAI,WAAW;AACf,MAAI,KAAK,QAAQ;GACf,MAAM,YAAY,IAAI,WAAW,QAAQ,IAAI,QAAQ,KAAK,OAAO;AACjE,OAAI,cAAc,GAChB,YAAW,YAAY;;EAG3B,MAAM,OAAO,IAAI,MAAM,UAAU,WAAW,QAAQ,EAAE;EACtD,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,UAAU,KAAK,MAAM,GAAG,MAAM,GAAG;AAE/C,SAAO;GAAE;GAAO,YADG,UAAU,MAAM,MAAM,SAAS,GAAG,MAAM;GAC/B;;CAE/B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BF,MAAa,cAAc,SAAS;CAClC,MAAM;EAAE,SAAS,EAAE,GAAG,QAAQ;EAAE,MAAM,EAAE,KAAK;EAAE;CAC/C,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,SAAS,WAAW;AAEzC,MAAI,KAAK,kBAAkB,QAAW;GAEpC,MAAM,kBADW,MAAM,IAAI,GAAG,IAAI,SAAS,QAAQ,GAClB;GACjC,MAAM,mBAAmB,KAAK;GAC9B,MAAM,YAAY,CAAC;GACnB,IAAI;AACJ,OAAI,UACF,kBAAiB;OAGjB,mBADe,MAAM,IAAI,GAAG,IAAI,SAAS,iBAAkB,GAClC,eAAe;AAE1C,QAAK,SAAS;AACd,QAAK,cAAc;AAEnB,OAAI,kBAAkB,mBAAmB,gBAAgB;IACvD,MAAM,cAAc,MAAM,IAAI,GAC3B,MAAM,QAAQ,CACd,UAAU,kBAAkB,MAC3B,EAAE,GAAG,eAAe,eAAe,CACpC,CACA,SAAS;AACZ,SAAK,MAAM,QAAQ,YACjB,KAAI,KAAK,QAAQ,QACf,OAAM,IAAI,GAAG,MAAM,SAAS,KAAK,KAAK,EACpC,aAAa,gBACd,CAAC;;;AAMV,MAAI,KAAK,SAAS,QAAW;GAC3B,MAAM,iBAA4B,MAAM,QAAQ,KAAK,KAAK,GACtD,cAAc,KAAK,KAAkB,GACrC,EAAE;GAEN,MAAM,eAAe,MAAM,IAAI,GAC5B,MAAM,WAAW,CACjB,UAAU,aAAa,QAAQ,IAAI,GAAG,YAAY,QAAQ,CAAC,CAC3D,SAAS;AACZ,QAAK,MAAM,YAAY,aACrB,OAAM,IAAI,GAAG,OAAO,YAAY,SAAS,IAAI;AAG/C,QAAK,MAAM,OAAO,eAChB,OAAM,IAAI,GAAG,OAAO,YAAY;IAC9B,UAAU;IACV,KAAK,IAAI;IACT,OAAO,IAAI;IACZ,CAAC;AAGJ,SAAM,IAAI,GAAG,MAAM,SAAS,SAAS;IACnC,GAAG;IACH,MAAM,eAAe,SAAS,IAAI,iBAAiB;IACpD,CAAC;QAEF,OAAM,IAAI,GAAG,MAAM,SAAS,SAAS,KAAK;AAE5C,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBF,MAAa,cAAc,SAAS;CAClC,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE;CAChC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,cAAc;EACnC,MAAM,cAAc,OAAO,OAAuB;GAChD,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,QAAQ,CACd,UAAU,oBAAoB,MAAM,EAAE,GAAG,iBAAiB,GAAG,CAAC,CAC9D,SAAS;AACZ,QAAK,MAAM,SAAS,SAClB,OAAM,YAAY,MAAM,IAAI;GAG9B,MAAM,UAAU,MAAM,IAAI,GACvB,MAAM,cAAc,CACpB,UAAU,aAAa,MAAM,EAAE,GAAG,WAAW,GAAG,CAAC,CACjD,SAAS;AACZ,QAAK,MAAM,UAAU,QACnB,OAAM,IAAI,GAAG,OAAO,eAAe,OAAO,IAAI;GAGhD,MAAM,UAAU,MAAM,IAAI,GACvB,MAAM,cAAc,CACpB,UAAU,aAAa,MAAM,EAAE,GAAG,WAAW,GAAG,CAAC,CACjD,SAAS;AACZ,QAAK,MAAM,UAAU,QACnB,OAAM,IAAI,GAAG,OAAO,eAAe,OAAO,IAAI;GAIhD,MAAM,OAAO,MAAM,IAAI,GACpB,MAAM,WAAW,CACjB,UAAU,aAAa,MAAM,EAAE,GAAG,YAAY,GAAG,CAAC,CAClD,SAAS;AACZ,QAAK,MAAM,OAAO,KAChB,OAAM,IAAI,GAAG,OAAO,YAAY,IAAI,IAAI;AAG1C,SAAM,IAAI,GAAG,OAAO,SAAS,GAAG;;AAGlC,QAAM,YAAY,QAAQ;AAC1B,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"core.js","names":[],"sources":["../../../../src/component/public/groups/core.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport type { Id } from \"../../_generated/dataModel\";\nimport { mutation, query } from \"../../functions\";\nimport { vGroupDoc, vPaginated, vTag } from \"../../model\";\n\ntype TagPair = { key: string; value: string };\n\nfunction normalizeTag(tag: TagPair): TagPair {\n return {\n key: tag.key.trim().toLowerCase(),\n value: tag.value.trim().toLowerCase(),\n };\n}\n\nfunction normalizeTags(tags: TagPair[]): TagPair[] {\n const seen = new Set<string>();\n const result: TagPair[] = [];\n for (const raw of tags) {\n const t = normalizeTag(raw);\n const composite = `${t.key}\\0${t.value}`;\n if (!seen.has(composite)) {\n seen.add(composite);\n result.push(t);\n }\n }\n return result;\n}\n/**\n * Create a new group. Groups are hierarchical — set `parentGroupId` to nest\n * under an existing group, or omit it to create a root-level group.\n *\n * Root groups self-reference their own ID as `rootGroupId`. Child groups\n * inherit `rootGroupId` from their parent chain. Tags are normalized\n * (trimmed and lowercased) and deduplicated before storage, and companion\n * `GroupTag` rows are created for indexed lookups.\n *\n * @param args.name - The display name for the group.\n * @param args.slug - An optional URL-friendly identifier for the group (e.g. `\"engineering\"`).\n * @param args.type - An optional application-defined group type (e.g. `\"organization\"`, `\"team\"`).\n * @param args.parentGroupId - The ID of an existing group to nest under. Omit to create a root-level group.\n * @param args.tags - An optional array of `{ key, value }` tag pairs to attach to the group for filtering.\n * @param args.extend - An optional arbitrary payload for application-specific metadata.\n * @returns The `Id<\"Group\">` of the newly created group document.\n *\n * @example\n * ```ts\n * const groupId = await ctx.runMutation(components.auth.groups.groupCreate, {\n * name: \"Acme Corp\",\n * slug: \"acme-corp\",\n * type: \"organization\",\n * tags: [{ key: \"plan\", value: \"enterprise\" }],\n * });\n * ```\n */\nexport const groupCreate = mutation({\n args: {\n name: v.string(),\n slug: v.optional(v.string()),\n type: v.optional(v.string()),\n parentGroupId: v.optional(v.id(\"Group\")),\n tags: v.optional(v.array(vTag)),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"Group\"),\n handler: async (ctx, args) => {\n const { tags: rawTags, ...rest } = args;\n const normalizedTags = rawTags ? normalizeTags(rawTags) : undefined;\n const isRoot = !args.parentGroupId;\n // Compute rootGroupId: root groups self-reference, children inherit from parent\n let rootGroupId: Id<\"Group\"> | undefined;\n if (!isRoot && args.parentGroupId) {\n const parent = await ctx.db.get(args.parentGroupId);\n rootGroupId = parent?.rootGroupId ?? args.parentGroupId;\n }\n const groupId = await ctx.db.insert(\"Group\", {\n ...rest,\n tags: normalizedTags,\n isRoot,\n rootGroupId: isRoot ? undefined : rootGroupId,\n });\n // Self-reference for root groups (need the ID after insert)\n if (isRoot) {\n await ctx.db.patch(groupId, { rootGroupId: groupId });\n }\n // Sync companion group_tag rows\n if (normalizedTags) {\n for (const tag of normalizedTags) {\n await ctx.db.insert(\"GroupTag\", {\n group_id: groupId,\n key: tag.key,\n value: tag.value,\n });\n }\n }\n return groupId;\n },\n});\n\n/**\n * Retrieve a group by its document ID.\n *\n * Performs a direct lookup in the `Group` table and returns the full group\n * document, or `null` if no group exists with the given ID.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to retrieve.\n * @returns The group document (including `name`, `slug`, `type`, `tags`, hierarchy fields, etc.) or `null` if not found.\n *\n * @example\n * ```ts\n * const group = await ctx.runQuery(components.auth.groups.groupGet, {\n * groupId: existingGroupId,\n * });\n * if (group !== null) {\n * console.log(group.name, group.slug);\n * }\n * ```\n */\nexport const groupGet = query({\n args: { groupId: v.id(\"Group\") },\n returns: v.union(vGroupDoc, v.null()),\n handler: async (ctx, { groupId }) => {\n return await ctx.db.get(\"Group\", groupId);\n },\n});\n\n/**\n * List groups with optional filtering, sorting, and pagination.\n *\n * Returns `{ items, nextCursor }`. Empty `where` returns **all** groups.\n * The query engine selects the best database index based on the combination\n * of filter fields provided. Tag filters (`tagsAll`, `tagsAny`) are resolved\n * via the `GroupTag` companion table and intersected/unioned with index results.\n *\n * @param args.where - Optional filter criteria for narrowing results.\n * @param args.where.slug - Match groups with this exact slug.\n * @param args.where.type - Match groups with this exact type.\n * @param args.where.parentGroupId - Match groups that are direct children of the specified parent group.\n * @param args.where.name - Match groups with this exact name.\n * @param args.where.isRoot - When `true`, return only root-level groups; when `false`, only child groups.\n * @param args.where.tagsAll - An array of `{ key, value }` pairs; only groups that have **all** of these tags are returned.\n * @param args.where.tagsAny - An array of `{ key, value }` pairs; groups that have **at least one** of these tags are returned.\n * @param args.limit - Maximum number of items per page (clamped to 1..100, defaults to 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor` to fetch the next page, or `null` to start from the beginning.\n * @param args.orderBy - The field to sort by: `\"_creationTime\"`, `\"name\"`, `\"slug\"`, or `\"type\"`.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (defaults to `\"desc\"`).\n * @returns An object `{ items, nextCursor }` where `items` is an array of group documents and `nextCursor` is `null` when there are no more pages.\n *\n * @example\n * ```ts\n * const { items, nextCursor } = await ctx.runQuery(\n * components.auth.groups.groupList,\n * {\n * where: { type: \"team\", isRoot: false },\n * limit: 20,\n * order: \"asc\",\n * },\n * );\n * ```\n */\nexport const groupList = query({\n args: {\n where: v.optional(\n v.object({\n slug: v.optional(v.string()),\n type: v.optional(v.string()),\n parentGroupId: v.optional(v.id(\"Group\")),\n name: v.optional(v.string()),\n isRoot: v.optional(v.boolean()),\n tagsAll: v.optional(v.array(vTag)),\n tagsAny: v.optional(v.array(vTag)),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n orderBy: v.optional(\n v.union(\n v.literal(\"_creationTime\"),\n v.literal(\"name\"),\n v.literal(\"slug\"),\n v.literal(\"type\"),\n ),\n ),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vGroupDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n // ---- Resolve tag filters into a Set<Id<\"Group\">> ----\n let tagFilteredIds: Set<string> | null = null;\n\n if (where.tagsAll && where.tagsAll.length > 0) {\n // Intersect: group must have ALL specified tags\n let allSet: Set<string> | null = null;\n for (const rawTag of where.tagsAll) {\n const t = normalizeTag(rawTag);\n const rows = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_key_value\", (idx) =>\n idx.eq(\"key\", t.key).eq(\"value\", t.value),\n )\n .collect();\n const ids = new Set(rows.map((r) => r.group_id as string));\n if (allSet === null) {\n allSet = ids;\n } else {\n // Intersect\n for (const id of allSet) {\n if (!ids.has(id)) allSet.delete(id);\n }\n }\n // Short-circuit: empty intersection\n if (allSet.size === 0) break;\n }\n tagFilteredIds = allSet ?? new Set();\n }\n\n if (where.tagsAny && where.tagsAny.length > 0) {\n // Union: group must have at least one of the specified tags\n const anySet = new Set<string>();\n for (const rawTag of where.tagsAny) {\n const t = normalizeTag(rawTag);\n const rows = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_key_value\", (idx) =>\n idx.eq(\"key\", t.key).eq(\"value\", t.value),\n )\n .collect();\n for (const r of rows) {\n anySet.add(r.group_id as string);\n }\n }\n if (tagFilteredIds !== null) {\n // AND with tagsAll result\n for (const id of tagFilteredIds) {\n if (!anySet.has(id)) tagFilteredIds.delete(id);\n }\n } else {\n tagFilteredIds = anySet;\n }\n }\n\n // ---- Pick best index based on non-tag where fields ----\n let q;\n if (where.type !== undefined && where.parentGroupId !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"type_parent_group_id\", (idx) =>\n idx.eq(\"type\", where.type!).eq(\"parentGroupId\", where.parentGroupId!),\n );\n } else if (where.slug !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"slug\", (idx) => idx.eq(\"slug\", where.slug!));\n } else if (where.type !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"type\", (idx) => idx.eq(\"type\", where.type!));\n } else if (where.parentGroupId !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"parent_group_id\", (idx) =>\n idx.eq(\"parentGroupId\", where.parentGroupId!),\n );\n } else if (where.isRoot !== undefined) {\n q = ctx.db\n .query(\"Group\")\n .withIndex(\"is_root\", (idx) => idx.eq(\"isRoot\", where.isRoot!));\n } else {\n q = ctx.db.query(\"Group\");\n }\n\n // Apply remaining non-tag filters not covered by index\n if (where.name !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"name\"), where.name!));\n }\n // isRoot filter when not already used as primary index\n if (\n where.isRoot !== undefined &&\n where.parentGroupId === undefined &&\n (where.type !== undefined || where.slug !== undefined)\n ) {\n q = q.filter((f) => f.eq(f.field(\"isRoot\"), where.isRoot!));\n }\n // slug filter when not used as index\n if (where.slug !== undefined && where.type !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"slug\"), where.slug!));\n }\n\n q = q.order(order);\n\n let all = await q.collect();\n\n // Apply tag filter (intersect with resolved groupIds)\n if (tagFilteredIds !== null) {\n all = all.filter((doc) => tagFilteredIds!.has(doc._id as string));\n }\n\n // Cursor-based pagination\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Update a group's mutable fields such as `name`, `slug`, `tags`, `extend`,\n * and `parentGroupId`.\n *\n * When `parentGroupId` is changed the mutation automatically recomputes\n * `isRoot` and `rootGroupId` for the target group **and** cascades the new\n * `rootGroupId` to all descendant groups. When `tags` are provided they are\n * normalized, deduplicated, and the companion `GroupTag` rows are fully\n * replaced (delete-then-insert).\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to update.\n * @param args.data - A partial object of fields to patch. Supported keys include `name`, `slug`, `type`, `parentGroupId`, `tags`, and `extend`.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.groupUpdate, {\n * groupId: existingGroupId,\n * data: {\n * name: \"Acme Corp (renamed)\",\n * tags: [{ key: \"plan\", value: \"pro\" }],\n * },\n * });\n * ```\n */\nexport const groupUpdate = mutation({\n args: { groupId: v.id(\"Group\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { groupId, data }) => {\n // If parentGroupId is changing, recompute rootGroupId + isRoot for this group and descendants\n if (data.parentGroupId !== undefined) {\n const oldGroup = await ctx.db.get(\"Group\", groupId);\n const oldRootGroupId = oldGroup?.rootGroupId;\n const newParentGroupId = data.parentGroupId as Id<\"Group\"> | undefined;\n const newIsRoot = !newParentGroupId;\n let newRootGroupId: Id<\"Group\">;\n if (newIsRoot) {\n newRootGroupId = groupId;\n } else {\n const parent = await ctx.db.get(\"Group\", newParentGroupId!);\n newRootGroupId = parent?.rootGroupId ?? newParentGroupId!;\n }\n data.isRoot = newIsRoot;\n data.rootGroupId = newRootGroupId;\n // Cascade to descendants if rootGroupId changed\n if (oldRootGroupId && oldRootGroupId !== newRootGroupId) {\n const descendants = await ctx.db\n .query(\"Group\")\n .withIndex(\"root_group_id\", (q) =>\n q.eq(\"rootGroupId\", oldRootGroupId),\n )\n .collect();\n for (const desc of descendants) {\n if (desc._id !== groupId) {\n await ctx.db.patch(\"Group\", desc._id, {\n rootGroupId: newRootGroupId,\n });\n }\n }\n }\n }\n // If tags are being updated, normalize and replace the full tag set\n if (data.tags !== undefined) {\n const normalizedTags: TagPair[] = Array.isArray(data.tags)\n ? normalizeTags(data.tags as TagPair[])\n : [];\n // Delete existing group_tag rows for this group\n const existingTags = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_group\", (idx) => idx.eq(\"group_id\", groupId))\n .collect();\n for (const existing of existingTags) {\n await ctx.db.delete(\"GroupTag\", existing._id);\n }\n // Insert new normalized group_tag rows\n for (const tag of normalizedTags) {\n await ctx.db.insert(\"GroupTag\", {\n group_id: groupId,\n key: tag.key,\n value: tag.value,\n });\n }\n // Patch group with normalized tags (empty array = clear all)\n await ctx.db.patch(\"Group\", groupId, {\n ...data,\n tags: normalizedTags.length > 0 ? normalizedTags : undefined,\n });\n } else {\n await ctx.db.patch(\"Group\", groupId, data);\n }\n return null;\n },\n});\n\n/**\n * Delete a group and all of its descendants. This cascades to:\n * - All child groups (recursively)\n * - All members of this group and its descendants\n * - All invites for this group and its descendants\n * - All companion `GroupTag` rows for this group and its descendants\n *\n * The deletion walks the group tree depth-first, removing leaves before\n * parents, so referential integrity is maintained throughout.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to delete. All children are deleted recursively.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // Delete an organization and everything nested under it\n * await ctx.runMutation(components.auth.groups.groupDelete, {\n * groupId: organizationGroupId,\n * });\n * ```\n */\nexport const groupDelete = mutation({\n args: { groupId: v.id(\"Group\") },\n returns: v.null(),\n handler: async (ctx, { groupId }) => {\n const deleteGroup = async (id: typeof groupId) => {\n const children = await ctx.db\n .query(\"Group\")\n .withIndex(\"parent_group_id\", (q) => q.eq(\"parentGroupId\", id))\n .collect();\n for (const child of children) {\n await deleteGroup(child._id);\n }\n\n const members = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id\", (q) => q.eq(\"groupId\", id))\n .collect();\n for (const member of members) {\n await ctx.db.delete(\"GroupMember\", member._id);\n }\n\n const invites = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id\", (q) => q.eq(\"groupId\", id))\n .collect();\n for (const invite of invites) {\n await ctx.db.delete(\"GroupInvite\", invite._id);\n }\n\n // Delete companion group_tag rows\n const tags = await ctx.db\n .query(\"GroupTag\")\n .withIndex(\"by_group\", (q) => q.eq(\"group_id\", id))\n .collect();\n for (const tag of tags) {\n await ctx.db.delete(\"GroupTag\", tag._id);\n }\n\n await ctx.db.delete(\"Group\", id);\n };\n\n await deleteGroup(groupId);\n return null;\n },\n});\n\n// ============================================================================\n// Members\n// ============================================================================\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"invites.d.ts","names":[],"sources":["../../../../src/component/public/groups/invites.ts"],"mappings":";;;;;;;;;;;;;;;AA8CA;;;;;AAiGA;;;;;AA6BA;;;;;AA+CA;;;;;AAmJA;;;;;AAkFA;;;;cAlZa,YAAA;AAohBb;;;;;;;;;;;;;;;;;;;AAAA,cAnba,SAAA;;;;;;;;;;;;;;;;;;;;;;cA6BA,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+CA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmJA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkFA,mBAAA;;;;;;;;;;;;;;;;;;;;;cAkIA,YAAA"}
1
+ {"version":3,"file":"invites.d.ts","names":[],"sources":["../../../../src/component/public/groups/invites.ts"],"mappings":";;;;;;;;;;;;;;;AAoDA;;;;;AAiGA;;;;;AA6BA;;;;;AA+CA;;;;;AAmJA;;;;;AAkFA;;;;cAlZa,YAAA;AAohBb;;;;;;;;;;;;;;;;;;;AAAA,cAnba,SAAA;;;;;;;;;;;;;;;;;;;;;;cA6BA,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+CA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmJA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkFA,mBAAA;;;;;;;;;;;;;;;;;;;;;cAkIA,YAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"invites.js","names":[],"sources":["../../../../src/component/public/groups/invites.ts"],"sourcesContent":["import { ConvexError, v } from \"convex/values\";\nimport type { Id } from \"../../_generated/dataModel\";\nimport { mutation, query } from \"../../functions\";\nimport { vGroupInviteDoc, vInviteAcceptByTokenResult, vInviteStatus, vPaginated } from \"../../model\";\n\n/**\n * Create a new platform-level invitation. Optionally set `groupId` to tie\n * the invite to a specific group. The invitation is sent to an email address\n * and includes a hashed token for secure acceptance.\n *\n * Throws `ConvexError` with code `DUPLICATE_INVITE` when a pending invite\n * already exists for the same email and scope:\n * - group invite: same `email` + same `groupId`\n * - platform invite: same `email` with no `groupId`\n *\n * When a duplicate check finds an existing invite that has passed its\n * `expiresTime`, that invite is automatically marked as `\"expired\"` and the\n * new invite is allowed through. CLI-generated invites (no email) skip\n * duplicate detection entirely.\n *\n * @param args.groupId - Optional `Id<\"Group\">` to scope this invite to a specific group. Omit for a platform-wide invite.\n * @param args.invitedByUserId - Optional `Id<\"User\">` of the user who issued the invitation.\n * @param args.email - Optional email address of the invitee. When provided, duplicate detection is enforced.\n * @param args.tokenHash - A pre-hashed token string used for secure, URL-safe invite acceptance.\n * @param args.roleIds - Optional array of application-defined role identifiers to assign upon acceptance.\n * @param args.status - The initial status of the invite (typically `\"pending\"`).\n * @param args.expiresTime - Optional Unix timestamp (ms) after which the invite is considered expired.\n * @param args.extend - Optional arbitrary payload for application-specific metadata.\n * @returns The `Id<\"GroupInvite\">` of the newly created invite document.\n *\n * @example\n * ```ts\n * const inviteId = await ctx.runMutation(\n * components.auth.groups.inviteCreate,\n * {\n * groupId: teamGroupId,\n * invitedByUserId: currentUserId,\n * email: \"alice@example.com\",\n * tokenHash: hashedToken,\n * roleIds: [\"editor\"],\n * status: \"pending\",\n * expiresTime: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days\n * },\n * );\n * ```\n */\nexport const inviteCreate = mutation({\n args: {\n groupId: v.optional(v.id(\"Group\")),\n invitedByUserId: v.optional(v.id(\"User\")),\n email: v.optional(v.string()),\n tokenHash: v.string(),\n roleIds: v.optional(v.array(v.string())),\n status: vInviteStatus,\n expiresTime: v.optional(v.number()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"GroupInvite\"),\n handler: async (ctx, args) => {\n const now = Date.now();\n\n // Only check for duplicates when an email is provided.\n // CLI-generated invites (no email) are always allowed.\n if (args.email !== undefined) {\n if (args.groupId !== undefined) {\n const existingGroupInvites = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id_status\", (q) =>\n q.eq(\"groupId\", args.groupId).eq(\"status\", \"pending\"),\n )\n .filter((q) => q.eq(q.field(\"email\"), args.email))\n .collect();\n\n for (const existingGroupInvite of existingGroupInvites) {\n const isExpired =\n existingGroupInvite.expiresTime !== undefined &&\n existingGroupInvite.expiresTime <= now;\n if (isExpired) {\n await ctx.db.patch(\"GroupInvite\", existingGroupInvite._id, {\n status: \"expired\",\n });\n continue;\n }\n throw new ConvexError({\n code: \"DUPLICATE_INVITE\",\n message:\n \"A pending invite already exists for this email in this group\",\n email: args.email,\n groupId: args.groupId,\n existingInviteId: existingGroupInvite._id,\n });\n }\n } else {\n const existingPlatformInvites = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"email_status\", (q) =>\n q.eq(\"email\", args.email).eq(\"status\", \"pending\"),\n )\n .filter((q) => q.eq(q.field(\"groupId\"), undefined))\n .collect();\n\n for (const existingPlatformInvite of existingPlatformInvites) {\n const isExpired =\n existingPlatformInvite.expiresTime !== undefined &&\n existingPlatformInvite.expiresTime <= now;\n if (isExpired) {\n await ctx.db.patch(\"GroupInvite\", existingPlatformInvite._id, {\n status: \"expired\",\n });\n continue;\n }\n throw new ConvexError({\n code: \"DUPLICATE_INVITE\",\n message: \"A pending platform invite already exists for this email\",\n email: args.email,\n existingInviteId: existingPlatformInvite._id,\n });\n }\n }\n }\n return await ctx.db.insert(\"GroupInvite\", args);\n },\n});\n\n/**\n * Retrieve an invite by its document ID.\n *\n * Performs a direct lookup in the `GroupInvite` table and returns the full\n * invite document, or `null` if no invite exists with the given ID.\n *\n * @param args.inviteId - The `Id<\"GroupInvite\">` of the invite to retrieve.\n * @returns The invite document (including `email`, `status`, `groupId`, `tokenHash`, etc.) or `null` if not found.\n *\n * @example\n * ```ts\n * const invite = await ctx.runQuery(components.auth.groups.inviteGet, {\n * inviteId: existingInviteId,\n * });\n * if (invite !== null) {\n * console.log(invite.email, invite.status);\n * }\n * ```\n */\nexport const inviteGet = query({\n args: { inviteId: v.id(\"GroupInvite\") },\n returns: v.union(vGroupInviteDoc, v.null()),\n handler: async (ctx, { inviteId }) => {\n return await ctx.db.get(\"GroupInvite\", inviteId);\n },\n});\n\n/**\n * Retrieve an invite by its hashed token.\n *\n * Looks up the `GroupInvite` table using the `token_hash` index. This is\n * the primary mechanism for resolving an invite from a URL-embedded token.\n * Returns `null` if no invite matches the given hash.\n *\n * @param args.tokenHash - The hashed token string to look up (must match the value stored at creation time).\n * @returns The invite document or `null` if no invite exists with the given token hash.\n *\n * @example\n * ```ts\n * const invite = await ctx.runQuery(\n * components.auth.groups.inviteGetByTokenHash,\n * { tokenHash: \"sha256_abc123...\" },\n * );\n * if (invite !== null && invite.status === \"pending\") {\n * // proceed with acceptance flow\n * }\n * ```\n */\nexport const inviteGetByTokenHash = query({\n args: { tokenHash: v.string() },\n returns: v.union(vGroupInviteDoc, v.null()),\n handler: async (ctx, { tokenHash }) => {\n return await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"token_hash\", (q) => q.eq(\"tokenHash\", tokenHash))\n .first();\n },\n});\n\n/**\n * List invites with optional filtering, sorting, and pagination.\n *\n * Returns `{ items, nextCursor }`. Supports filtering by `groupId`,\n * `status`, `email`, `invitedByUserId`, `roleId`, `acceptedByUserId`, and\n * `tokenHash`. The query engine automatically selects the best compound\n * index based on the combination of filter fields provided. The `roleId`\n * filter is applied in-memory after the index scan because role IDs are\n * stored as an array.\n *\n * @param args.where - Optional filter criteria for narrowing results.\n * @param args.where.tokenHash - Match invites with this exact hashed token.\n * @param args.where.groupId - Match invites scoped to this group.\n * @param args.where.status - Match invites with this status (e.g. `\"pending\"`, `\"accepted\"`, `\"revoked\"`).\n * @param args.where.email - Match invites sent to this email address.\n * @param args.where.invitedByUserId - Match invites created by this user.\n * @param args.where.roleId - Match invites that include this role identifier in their `roleIds` array.\n * @param args.where.acceptedByUserId - Match invites accepted by this specific user.\n * @param args.limit - Maximum number of items per page (clamped to 1..100, defaults to 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor` to fetch the next page, or `null` to start from the beginning.\n * @param args.orderBy - The field to sort by: `\"_creationTime\"`, `\"status\"`, `\"email\"`, `\"expiresTime\"`, or `\"acceptedTime\"`.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (defaults to `\"desc\"`).\n * @returns An object `{ items, nextCursor }` where `items` is an array of invite documents and `nextCursor` is `null` when there are no more pages.\n *\n * @example\n * ```ts\n * const { items, nextCursor } = await ctx.runQuery(\n * components.auth.groups.inviteList,\n * {\n * where: { groupId: teamGroupId, status: \"pending\" },\n * limit: 25,\n * order: \"desc\",\n * },\n * );\n * ```\n */\nexport const inviteList = query({\n args: {\n where: v.optional(\n v.object({\n tokenHash: v.optional(v.string()),\n groupId: v.optional(v.id(\"Group\")),\n status: v.optional(vInviteStatus),\n email: v.optional(v.string()),\n invitedByUserId: v.optional(v.id(\"User\")),\n roleId: v.optional(v.string()),\n acceptedByUserId: v.optional(v.id(\"User\")),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n orderBy: v.optional(\n v.union(\n v.literal(\"_creationTime\"),\n v.literal(\"status\"),\n v.literal(\"email\"),\n v.literal(\"expiresTime\"),\n v.literal(\"acceptedTime\"),\n ),\n ),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vGroupInviteDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n // Pick best index\n let q;\n if (where.tokenHash !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"token_hash\", (idx) =>\n idx.eq(\"tokenHash\", where.tokenHash!),\n );\n } else if (where.groupId !== undefined && where.status !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id_status\", (idx) =>\n idx.eq(\"groupId\", where.groupId!).eq(\"status\", where.status!),\n );\n } else if (where.email !== undefined && where.status !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"email_status\", (idx) =>\n idx.eq(\"email\", where.email!).eq(\"status\", where.status!),\n );\n } else if (\n where.invitedByUserId !== undefined &&\n where.status !== undefined\n ) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"invited_by_user_id_status\", (idx) =>\n idx\n .eq(\"invitedByUserId\", where.invitedByUserId!)\n .eq(\"status\", where.status!),\n );\n } else if (where.groupId !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id\", (idx) => idx.eq(\"groupId\", where.groupId!));\n } else if (where.status !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"status\", (idx) => idx.eq(\"status\", where.status!));\n } else {\n q = ctx.db.query(\"GroupInvite\");\n }\n\n // Apply remaining filters\n if (where.groupId !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"groupId\"), where.groupId!));\n }\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n if (where.email !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"email\"), where.email!));\n }\n if (where.invitedByUserId !== undefined) {\n q = q.filter((f) =>\n f.eq(f.field(\"invitedByUserId\"), where.invitedByUserId!),\n );\n }\n if (where.acceptedByUserId !== undefined) {\n q = q.filter((f) =>\n f.eq(f.field(\"acceptedByUserId\"), where.acceptedByUserId!),\n );\n }\n if (where.tokenHash !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"tokenHash\"), where.tokenHash!));\n }\n\n q = q.order(order);\n\n let all = await q.collect();\n if (where.roleId !== undefined) {\n all = all.filter((doc) => (doc.roleIds ?? []).includes(where.roleId!));\n }\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Accept a pending invitation.\n *\n * Marks the invite as `\"accepted\"` and records the acceptance timestamp.\n * Throws a structured `ConvexError` when the invite doesn't exist or is not\n * currently pending. If the invite has passed its `expiresTime`, it is\n * automatically transitioned to `\"expired\"` and the acceptance is rejected.\n *\n * The caller is responsible for creating the corresponding member record\n * (see {@link inviteAcceptByToken} for an all-in-one alternative that also\n * handles membership).\n *\n * @param args.inviteId - The `Id<\"GroupInvite\">` of the invite to accept.\n * @param args.acceptedByUserId - Optional `Id<\"User\">` of the user accepting the invite. Stored on the invite for audit purposes.\n * @returns `null` on success.\n * @throws `ConvexError` with code `INVITE_NOT_FOUND` if the invite does not exist.\n * @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has already been accepted, revoked, or otherwise finalized.\n * @throws `ConvexError` with code `INVITE_EXPIRED` if the invite's `expiresTime` has passed.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.inviteAccept, {\n * inviteId: pendingInviteId,\n * acceptedByUserId: currentUserId,\n * });\n * ```\n */\nexport const inviteAccept = mutation({\n args: {\n inviteId: v.id(\"GroupInvite\"),\n acceptedByUserId: v.optional(v.id(\"User\")),\n },\n returns: v.null(),\n handler: async (ctx, { inviteId, acceptedByUserId }) => {\n const invite = await ctx.db.get(\"GroupInvite\", inviteId);\n if (invite === null) {\n throw new ConvexError({\n code: \"INVITE_NOT_FOUND\",\n message: \"Invite not found\",\n inviteId,\n });\n }\n if (invite.status !== \"pending\") {\n throw new ConvexError({\n code: \"INVITE_NOT_PENDING\",\n message: `Cannot accept invite with status \"${invite.status}\"`,\n inviteId,\n currentStatus: invite.status,\n });\n }\n if (invite.expiresTime !== undefined && invite.expiresTime <= Date.now()) {\n await ctx.db.patch(\"GroupInvite\", inviteId, {\n status: \"expired\",\n });\n throw new ConvexError({\n code: \"INVITE_EXPIRED\",\n message: \"Invite has expired\",\n inviteId,\n });\n }\n await ctx.db.patch(\"GroupInvite\", inviteId, {\n status: \"accepted\",\n acceptedTime: Date.now(),\n ...(acceptedByUserId ? { acceptedByUserId } : {}),\n });\n return null;\n },\n});\n\n/**\n * Accept an invitation by raw token hash and atomically join group membership.\n *\n * This is the primary token-based acceptance flow. It looks up the invite by\n * `tokenHash`, validates status and expiry, verifies the accepting user's\n * email matches the invite email (when set), and — if the invite is scoped\n * to a group — creates a `GroupMember` record in the same transaction.\n *\n * The operation is idempotent: if the invite was already accepted by the\n * same user, it returns a result with `inviteStatus: \"already_accepted\"`\n * and the existing membership information.\n *\n * @param args.tokenHash - The hashed token string that identifies the invite (typically extracted from an invite URL).\n * @param args.acceptedByUserId - The `Id<\"User\">` of the user accepting the invitation. Their email must match the invite's email when one was specified.\n * @returns An object describing the outcome:\n * - `inviteId` — the ID of the accepted invite.\n * - `groupId` — the group the invite targets, or `null` for platform invites.\n * - `memberId` — the ID of the created (or existing) member record, or `undefined` for platform invites.\n * - `inviteStatus` — `\"accepted\"` for a fresh acceptance, `\"already_accepted\"` for idempotent replays.\n * - `membershipStatus` — `\"joined\"`, `\"already_joined\"`, or `\"not_applicable\"`.\n * @throws `ConvexError` with code `INVITE_NOT_FOUND` if no invite matches the token hash.\n * @throws `ConvexError` with code `INVITE_EXPIRED` if the invite's `expiresTime` has passed.\n * @throws `ConvexError` with code `INVITE_ALREADY_ACCEPTED` if the invite was accepted by a different user.\n * @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has been revoked or is in another non-pending state.\n * @throws `ConvexError` with code `INVITE_EMAIL_MISMATCH` if the accepting user's email does not match the invite's email.\n *\n * @example\n * ```ts\n * const result = await ctx.runMutation(\n * components.auth.groups.inviteAcceptByToken,\n * {\n * tokenHash: \"sha256_abc123...\",\n * acceptedByUserId: currentUserId,\n * },\n * );\n * if (result.membershipStatus === \"joined\") {\n * console.log(\"Joined group\", result.groupId, \"as member\", result.memberId);\n * }\n * ```\n */\nexport const inviteAcceptByToken = mutation({\n args: {\n tokenHash: v.string(),\n acceptedByUserId: v.id(\"User\"),\n },\n returns: vInviteAcceptByTokenResult,\n handler: async (ctx, { tokenHash, acceptedByUserId }) => {\n const invite = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"token_hash\", (q) => q.eq(\"tokenHash\", tokenHash))\n .first();\n\n if (invite === null) {\n throw new ConvexError({\n code: \"INVITE_NOT_FOUND\",\n message: \"Invite not found\",\n });\n }\n\n const now = Date.now();\n if (invite.status === \"pending\") {\n if (invite.expiresTime !== undefined && invite.expiresTime <= now) {\n await ctx.db.patch(\"GroupInvite\", invite._id, { status: \"expired\" });\n throw new ConvexError({\n code: \"INVITE_EXPIRED\",\n message: \"Invite has expired\",\n inviteId: invite._id,\n });\n }\n } else if (invite.status === \"accepted\") {\n if (invite.acceptedByUserId !== acceptedByUserId) {\n throw new ConvexError({\n code: \"INVITE_ALREADY_ACCEPTED\",\n message: \"Invite already accepted by another user\",\n inviteId: invite._id,\n });\n }\n } else {\n throw new ConvexError({\n code: \"INVITE_NOT_PENDING\",\n message: `Cannot accept invite with status \"${invite.status}\"`,\n inviteId: invite._id,\n currentStatus: invite.status,\n });\n }\n\n if (invite.email !== undefined) {\n const user = await ctx.db.get(\"User\", acceptedByUserId);\n const normalizedInviteEmail = invite.email.trim().toLowerCase();\n const normalizedUserEmail = user?.email?.trim().toLowerCase();\n\n if (\n normalizedUserEmail === undefined ||\n normalizedUserEmail !== normalizedInviteEmail\n ) {\n throw new ConvexError({\n code: \"INVITE_EMAIL_MISMATCH\",\n message: \"Invite email does not match accepting user's email\",\n inviteId: invite._id,\n });\n }\n }\n\n let membershipStatus: \"joined\" | \"already_joined\" | \"not_applicable\" =\n \"not_applicable\";\n let memberId: Id<\"GroupMember\"> | undefined;\n\n if (invite.groupId !== undefined) {\n const existingMembership = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", invite.groupId!).eq(\"userId\", acceptedByUserId),\n )\n .unique();\n\n if (existingMembership !== null) {\n membershipStatus = \"already_joined\";\n memberId = existingMembership._id;\n } else {\n memberId = await ctx.db.insert(\"GroupMember\", {\n groupId: invite.groupId,\n userId: acceptedByUserId,\n roleIds: invite.roleIds,\n status: \"active\",\n });\n membershipStatus = \"joined\";\n }\n }\n\n if (invite.status === \"pending\") {\n await ctx.db.patch(\"GroupInvite\", invite._id, {\n status: \"accepted\",\n acceptedByUserId,\n acceptedTime: now,\n });\n }\n\n const inviteStatus: \"accepted\" | \"already_accepted\" =\n invite.status === \"accepted\" ? \"already_accepted\" : \"accepted\";\n\n return {\n inviteId: invite._id,\n groupId: invite.groupId ?? null,\n memberId,\n inviteStatus,\n membershipStatus,\n };\n },\n});\n\n/**\n * Revoke a pending invitation.\n *\n * Marks the invite as `\"revoked\"`. Only invites with status `\"pending\"` can\n * be revoked. Throws a structured `ConvexError` when the invite doesn't\n * exist or is not currently pending. Once revoked, the invite's token can\n * no longer be used for acceptance.\n *\n * @param args.inviteId - The `Id<\"GroupInvite\">` of the invite to revoke.\n * @returns `null` on success.\n * @throws `ConvexError` with code `INVITE_NOT_FOUND` if the invite does not exist.\n * @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has already been accepted, revoked, or expired.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.inviteRevoke, {\n * inviteId: pendingInviteId,\n * });\n * ```\n */\nexport const inviteRevoke = mutation({\n args: { inviteId: v.id(\"GroupInvite\") },\n returns: v.null(),\n handler: async (ctx, { inviteId }) => {\n const invite = await ctx.db.get(\"GroupInvite\", inviteId);\n if (invite === null) {\n throw new ConvexError({\n code: \"INVITE_NOT_FOUND\",\n message: \"Invite not found\",\n inviteId,\n });\n }\n if (invite.status !== \"pending\") {\n throw new ConvexError({\n code: \"INVITE_NOT_PENDING\",\n message: `Cannot revoke invite with status \"${invite.status}\"`,\n inviteId,\n currentStatus: invite.status,\n });\n }\n await ctx.db.patch(\"GroupInvite\", inviteId, { status: \"revoked\" });\n return null;\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"invites.js","names":[],"sources":["../../../../src/component/public/groups/invites.ts"],"sourcesContent":["import { ConvexError, v } from \"convex/values\";\n\nimport type { Id } from \"../../_generated/dataModel\";\nimport { mutation, query } from \"../../functions\";\nimport {\n vGroupInviteDoc,\n vInviteAcceptByTokenResult,\n vInviteStatus,\n vPaginated,\n} from \"../../model\";\n\n/**\n * Create a new platform-level invitation. Optionally set `groupId` to tie\n * the invite to a specific group. The invitation is sent to an email address\n * and includes a hashed token for secure acceptance.\n *\n * Throws `ConvexError` with code `DUPLICATE_INVITE` when a pending invite\n * already exists for the same email and scope:\n * - group invite: same `email` + same `groupId`\n * - platform invite: same `email` with no `groupId`\n *\n * When a duplicate check finds an existing invite that has passed its\n * `expiresTime`, that invite is automatically marked as `\"expired\"` and the\n * new invite is allowed through. CLI-generated invites (no email) skip\n * duplicate detection entirely.\n *\n * @param args.groupId - Optional `Id<\"Group\">` to scope this invite to a specific group. Omit for a platform-wide invite.\n * @param args.invitedByUserId - Optional `Id<\"User\">` of the user who issued the invitation.\n * @param args.email - Optional email address of the invitee. When provided, duplicate detection is enforced.\n * @param args.tokenHash - A pre-hashed token string used for secure, URL-safe invite acceptance.\n * @param args.roleIds - Optional array of application-defined role identifiers to assign upon acceptance.\n * @param args.status - The initial status of the invite (typically `\"pending\"`).\n * @param args.expiresTime - Optional Unix timestamp (ms) after which the invite is considered expired.\n * @param args.extend - Optional arbitrary payload for application-specific metadata.\n * @returns The `Id<\"GroupInvite\">` of the newly created invite document.\n *\n * @example\n * ```ts\n * const inviteId = await ctx.runMutation(\n * components.auth.groups.inviteCreate,\n * {\n * groupId: teamGroupId,\n * invitedByUserId: currentUserId,\n * email: \"alice@example.com\",\n * tokenHash: hashedToken,\n * roleIds: [\"editor\"],\n * status: \"pending\",\n * expiresTime: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days\n * },\n * );\n * ```\n */\nexport const inviteCreate = mutation({\n args: {\n groupId: v.optional(v.id(\"Group\")),\n invitedByUserId: v.optional(v.id(\"User\")),\n email: v.optional(v.string()),\n tokenHash: v.string(),\n roleIds: v.optional(v.array(v.string())),\n status: vInviteStatus,\n expiresTime: v.optional(v.number()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"GroupInvite\"),\n handler: async (ctx, args) => {\n const now = Date.now();\n\n // Only check for duplicates when an email is provided.\n // CLI-generated invites (no email) are always allowed.\n if (args.email !== undefined) {\n if (args.groupId !== undefined) {\n const existingGroupInvites = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id_status\", (q) =>\n q.eq(\"groupId\", args.groupId).eq(\"status\", \"pending\"),\n )\n .filter((q) => q.eq(q.field(\"email\"), args.email))\n .collect();\n\n for (const existingGroupInvite of existingGroupInvites) {\n const isExpired =\n existingGroupInvite.expiresTime !== undefined &&\n existingGroupInvite.expiresTime <= now;\n if (isExpired) {\n await ctx.db.patch(\"GroupInvite\", existingGroupInvite._id, {\n status: \"expired\",\n });\n continue;\n }\n throw new ConvexError({\n code: \"DUPLICATE_INVITE\",\n message:\n \"A pending invite already exists for this email in this group\",\n email: args.email,\n groupId: args.groupId,\n existingInviteId: existingGroupInvite._id,\n });\n }\n } else {\n const existingPlatformInvites = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"email_status\", (q) =>\n q.eq(\"email\", args.email).eq(\"status\", \"pending\"),\n )\n .filter((q) => q.eq(q.field(\"groupId\"), undefined))\n .collect();\n\n for (const existingPlatformInvite of existingPlatformInvites) {\n const isExpired =\n existingPlatformInvite.expiresTime !== undefined &&\n existingPlatformInvite.expiresTime <= now;\n if (isExpired) {\n await ctx.db.patch(\"GroupInvite\", existingPlatformInvite._id, {\n status: \"expired\",\n });\n continue;\n }\n throw new ConvexError({\n code: \"DUPLICATE_INVITE\",\n message: \"A pending platform invite already exists for this email\",\n email: args.email,\n existingInviteId: existingPlatformInvite._id,\n });\n }\n }\n }\n return await ctx.db.insert(\"GroupInvite\", args);\n },\n});\n\n/**\n * Retrieve an invite by its document ID.\n *\n * Performs a direct lookup in the `GroupInvite` table and returns the full\n * invite document, or `null` if no invite exists with the given ID.\n *\n * @param args.inviteId - The `Id<\"GroupInvite\">` of the invite to retrieve.\n * @returns The invite document (including `email`, `status`, `groupId`, `tokenHash`, etc.) or `null` if not found.\n *\n * @example\n * ```ts\n * const invite = await ctx.runQuery(components.auth.groups.inviteGet, {\n * inviteId: existingInviteId,\n * });\n * if (invite !== null) {\n * console.log(invite.email, invite.status);\n * }\n * ```\n */\nexport const inviteGet = query({\n args: { inviteId: v.id(\"GroupInvite\") },\n returns: v.union(vGroupInviteDoc, v.null()),\n handler: async (ctx, { inviteId }) => {\n return await ctx.db.get(\"GroupInvite\", inviteId);\n },\n});\n\n/**\n * Retrieve an invite by its hashed token.\n *\n * Looks up the `GroupInvite` table using the `token_hash` index. This is\n * the primary mechanism for resolving an invite from a URL-embedded token.\n * Returns `null` if no invite matches the given hash.\n *\n * @param args.tokenHash - The hashed token string to look up (must match the value stored at creation time).\n * @returns The invite document or `null` if no invite exists with the given token hash.\n *\n * @example\n * ```ts\n * const invite = await ctx.runQuery(\n * components.auth.groups.inviteGetByTokenHash,\n * { tokenHash: \"sha256_abc123...\" },\n * );\n * if (invite !== null && invite.status === \"pending\") {\n * // proceed with acceptance flow\n * }\n * ```\n */\nexport const inviteGetByTokenHash = query({\n args: { tokenHash: v.string() },\n returns: v.union(vGroupInviteDoc, v.null()),\n handler: async (ctx, { tokenHash }) => {\n return await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"token_hash\", (q) => q.eq(\"tokenHash\", tokenHash))\n .first();\n },\n});\n\n/**\n * List invites with optional filtering, sorting, and pagination.\n *\n * Returns `{ items, nextCursor }`. Supports filtering by `groupId`,\n * `status`, `email`, `invitedByUserId`, `roleId`, `acceptedByUserId`, and\n * `tokenHash`. The query engine automatically selects the best compound\n * index based on the combination of filter fields provided. The `roleId`\n * filter is applied in-memory after the index scan because role IDs are\n * stored as an array.\n *\n * @param args.where - Optional filter criteria for narrowing results.\n * @param args.where.tokenHash - Match invites with this exact hashed token.\n * @param args.where.groupId - Match invites scoped to this group.\n * @param args.where.status - Match invites with this status (e.g. `\"pending\"`, `\"accepted\"`, `\"revoked\"`).\n * @param args.where.email - Match invites sent to this email address.\n * @param args.where.invitedByUserId - Match invites created by this user.\n * @param args.where.roleId - Match invites that include this role identifier in their `roleIds` array.\n * @param args.where.acceptedByUserId - Match invites accepted by this specific user.\n * @param args.limit - Maximum number of items per page (clamped to 1..100, defaults to 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor` to fetch the next page, or `null` to start from the beginning.\n * @param args.orderBy - The field to sort by: `\"_creationTime\"`, `\"status\"`, `\"email\"`, `\"expiresTime\"`, or `\"acceptedTime\"`.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (defaults to `\"desc\"`).\n * @returns An object `{ items, nextCursor }` where `items` is an array of invite documents and `nextCursor` is `null` when there are no more pages.\n *\n * @example\n * ```ts\n * const { items, nextCursor } = await ctx.runQuery(\n * components.auth.groups.inviteList,\n * {\n * where: { groupId: teamGroupId, status: \"pending\" },\n * limit: 25,\n * order: \"desc\",\n * },\n * );\n * ```\n */\nexport const inviteList = query({\n args: {\n where: v.optional(\n v.object({\n tokenHash: v.optional(v.string()),\n groupId: v.optional(v.id(\"Group\")),\n status: v.optional(vInviteStatus),\n email: v.optional(v.string()),\n invitedByUserId: v.optional(v.id(\"User\")),\n roleId: v.optional(v.string()),\n acceptedByUserId: v.optional(v.id(\"User\")),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n orderBy: v.optional(\n v.union(\n v.literal(\"_creationTime\"),\n v.literal(\"status\"),\n v.literal(\"email\"),\n v.literal(\"expiresTime\"),\n v.literal(\"acceptedTime\"),\n ),\n ),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vGroupInviteDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n // Pick best index\n let q;\n if (where.tokenHash !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"token_hash\", (idx) =>\n idx.eq(\"tokenHash\", where.tokenHash!),\n );\n } else if (where.groupId !== undefined && where.status !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id_status\", (idx) =>\n idx.eq(\"groupId\", where.groupId!).eq(\"status\", where.status!),\n );\n } else if (where.email !== undefined && where.status !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"email_status\", (idx) =>\n idx.eq(\"email\", where.email!).eq(\"status\", where.status!),\n );\n } else if (\n where.invitedByUserId !== undefined &&\n where.status !== undefined\n ) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"invited_by_user_id_status\", (idx) =>\n idx\n .eq(\"invitedByUserId\", where.invitedByUserId!)\n .eq(\"status\", where.status!),\n );\n } else if (where.groupId !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"group_id\", (idx) => idx.eq(\"groupId\", where.groupId!));\n } else if (where.status !== undefined) {\n q = ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"status\", (idx) => idx.eq(\"status\", where.status!));\n } else {\n q = ctx.db.query(\"GroupInvite\");\n }\n\n // Apply remaining filters\n if (where.groupId !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"groupId\"), where.groupId!));\n }\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n if (where.email !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"email\"), where.email!));\n }\n if (where.invitedByUserId !== undefined) {\n q = q.filter((f) =>\n f.eq(f.field(\"invitedByUserId\"), where.invitedByUserId!),\n );\n }\n if (where.acceptedByUserId !== undefined) {\n q = q.filter((f) =>\n f.eq(f.field(\"acceptedByUserId\"), where.acceptedByUserId!),\n );\n }\n if (where.tokenHash !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"tokenHash\"), where.tokenHash!));\n }\n\n q = q.order(order);\n\n let all = await q.collect();\n if (where.roleId !== undefined) {\n all = all.filter((doc) => (doc.roleIds ?? []).includes(where.roleId!));\n }\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Accept a pending invitation.\n *\n * Marks the invite as `\"accepted\"` and records the acceptance timestamp.\n * Throws a structured `ConvexError` when the invite doesn't exist or is not\n * currently pending. If the invite has passed its `expiresTime`, it is\n * automatically transitioned to `\"expired\"` and the acceptance is rejected.\n *\n * The caller is responsible for creating the corresponding member record\n * (see {@link inviteAcceptByToken} for an all-in-one alternative that also\n * handles membership).\n *\n * @param args.inviteId - The `Id<\"GroupInvite\">` of the invite to accept.\n * @param args.acceptedByUserId - Optional `Id<\"User\">` of the user accepting the invite. Stored on the invite for audit purposes.\n * @returns `null` on success.\n * @throws `ConvexError` with code `INVITE_NOT_FOUND` if the invite does not exist.\n * @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has already been accepted, revoked, or otherwise finalized.\n * @throws `ConvexError` with code `INVITE_EXPIRED` if the invite's `expiresTime` has passed.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.inviteAccept, {\n * inviteId: pendingInviteId,\n * acceptedByUserId: currentUserId,\n * });\n * ```\n */\nexport const inviteAccept = mutation({\n args: {\n inviteId: v.id(\"GroupInvite\"),\n acceptedByUserId: v.optional(v.id(\"User\")),\n },\n returns: v.null(),\n handler: async (ctx, { inviteId, acceptedByUserId }) => {\n const invite = await ctx.db.get(\"GroupInvite\", inviteId);\n if (invite === null) {\n throw new ConvexError({\n code: \"INVITE_NOT_FOUND\",\n message: \"Invite not found\",\n inviteId,\n });\n }\n if (invite.status !== \"pending\") {\n throw new ConvexError({\n code: \"INVITE_NOT_PENDING\",\n message: `Cannot accept invite with status \"${invite.status}\"`,\n inviteId,\n currentStatus: invite.status,\n });\n }\n if (invite.expiresTime !== undefined && invite.expiresTime <= Date.now()) {\n await ctx.db.patch(\"GroupInvite\", inviteId, {\n status: \"expired\",\n });\n throw new ConvexError({\n code: \"INVITE_EXPIRED\",\n message: \"Invite has expired\",\n inviteId,\n });\n }\n await ctx.db.patch(\"GroupInvite\", inviteId, {\n status: \"accepted\",\n acceptedTime: Date.now(),\n ...(acceptedByUserId ? { acceptedByUserId } : {}),\n });\n return null;\n },\n});\n\n/**\n * Accept an invitation by raw token hash and atomically join group membership.\n *\n * This is the primary token-based acceptance flow. It looks up the invite by\n * `tokenHash`, validates status and expiry, verifies the accepting user's\n * email matches the invite email (when set), and — if the invite is scoped\n * to a group — creates a `GroupMember` record in the same transaction.\n *\n * The operation is idempotent: if the invite was already accepted by the\n * same user, it returns a result with `inviteStatus: \"already_accepted\"`\n * and the existing membership information.\n *\n * @param args.tokenHash - The hashed token string that identifies the invite (typically extracted from an invite URL).\n * @param args.acceptedByUserId - The `Id<\"User\">` of the user accepting the invitation. Their email must match the invite's email when one was specified.\n * @returns An object describing the outcome:\n * - `inviteId` — the ID of the accepted invite.\n * - `groupId` — the group the invite targets, or `null` for platform invites.\n * - `memberId` — the ID of the created (or existing) member record, or `undefined` for platform invites.\n * - `inviteStatus` — `\"accepted\"` for a fresh acceptance, `\"already_accepted\"` for idempotent replays.\n * - `membershipStatus` — `\"joined\"`, `\"already_joined\"`, or `\"not_applicable\"`.\n * @throws `ConvexError` with code `INVITE_NOT_FOUND` if no invite matches the token hash.\n * @throws `ConvexError` with code `INVITE_EXPIRED` if the invite's `expiresTime` has passed.\n * @throws `ConvexError` with code `INVITE_ALREADY_ACCEPTED` if the invite was accepted by a different user.\n * @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has been revoked or is in another non-pending state.\n * @throws `ConvexError` with code `INVITE_EMAIL_MISMATCH` if the accepting user's email does not match the invite's email.\n *\n * @example\n * ```ts\n * const result = await ctx.runMutation(\n * components.auth.groups.inviteAcceptByToken,\n * {\n * tokenHash: \"sha256_abc123...\",\n * acceptedByUserId: currentUserId,\n * },\n * );\n * if (result.membershipStatus === \"joined\") {\n * console.log(\"Joined group\", result.groupId, \"as member\", result.memberId);\n * }\n * ```\n */\nexport const inviteAcceptByToken = mutation({\n args: {\n tokenHash: v.string(),\n acceptedByUserId: v.id(\"User\"),\n },\n returns: vInviteAcceptByTokenResult,\n handler: async (ctx, { tokenHash, acceptedByUserId }) => {\n const invite = await ctx.db\n .query(\"GroupInvite\")\n .withIndex(\"token_hash\", (q) => q.eq(\"tokenHash\", tokenHash))\n .first();\n\n if (invite === null) {\n throw new ConvexError({\n code: \"INVITE_NOT_FOUND\",\n message: \"Invite not found\",\n });\n }\n\n const now = Date.now();\n if (invite.status === \"pending\") {\n if (invite.expiresTime !== undefined && invite.expiresTime <= now) {\n await ctx.db.patch(\"GroupInvite\", invite._id, { status: \"expired\" });\n throw new ConvexError({\n code: \"INVITE_EXPIRED\",\n message: \"Invite has expired\",\n inviteId: invite._id,\n });\n }\n } else if (invite.status === \"accepted\") {\n if (invite.acceptedByUserId !== acceptedByUserId) {\n throw new ConvexError({\n code: \"INVITE_ALREADY_ACCEPTED\",\n message: \"Invite already accepted by another user\",\n inviteId: invite._id,\n });\n }\n } else {\n throw new ConvexError({\n code: \"INVITE_NOT_PENDING\",\n message: `Cannot accept invite with status \"${invite.status}\"`,\n inviteId: invite._id,\n currentStatus: invite.status,\n });\n }\n\n if (invite.email !== undefined) {\n const user = await ctx.db.get(\"User\", acceptedByUserId);\n const normalizedInviteEmail = invite.email.trim().toLowerCase();\n const normalizedUserEmail = user?.email?.trim().toLowerCase();\n\n if (\n normalizedUserEmail === undefined ||\n normalizedUserEmail !== normalizedInviteEmail\n ) {\n throw new ConvexError({\n code: \"INVITE_EMAIL_MISMATCH\",\n message: \"Invite email does not match accepting user's email\",\n inviteId: invite._id,\n });\n }\n }\n\n let membershipStatus: \"joined\" | \"already_joined\" | \"not_applicable\" =\n \"not_applicable\";\n let memberId: Id<\"GroupMember\"> | undefined;\n\n if (invite.groupId !== undefined) {\n const existingMembership = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", invite.groupId!).eq(\"userId\", acceptedByUserId),\n )\n .unique();\n\n if (existingMembership !== null) {\n membershipStatus = \"already_joined\";\n memberId = existingMembership._id;\n } else {\n memberId = await ctx.db.insert(\"GroupMember\", {\n groupId: invite.groupId,\n userId: acceptedByUserId,\n roleIds: invite.roleIds,\n status: \"active\",\n });\n membershipStatus = \"joined\";\n }\n }\n\n if (invite.status === \"pending\") {\n await ctx.db.patch(\"GroupInvite\", invite._id, {\n status: \"accepted\",\n acceptedByUserId,\n acceptedTime: now,\n });\n }\n\n const inviteStatus: \"accepted\" | \"already_accepted\" =\n invite.status === \"accepted\" ? \"already_accepted\" : \"accepted\";\n\n return {\n inviteId: invite._id,\n groupId: invite.groupId ?? null,\n memberId,\n inviteStatus,\n membershipStatus,\n };\n },\n});\n\n/**\n * Revoke a pending invitation.\n *\n * Marks the invite as `\"revoked\"`. Only invites with status `\"pending\"` can\n * be revoked. Throws a structured `ConvexError` when the invite doesn't\n * exist or is not currently pending. Once revoked, the invite's token can\n * no longer be used for acceptance.\n *\n * @param args.inviteId - The `Id<\"GroupInvite\">` of the invite to revoke.\n * @returns `null` on success.\n * @throws `ConvexError` with code `INVITE_NOT_FOUND` if the invite does not exist.\n * @throws `ConvexError` with code `INVITE_NOT_PENDING` if the invite has already been accepted, revoked, or expired.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.inviteRevoke, {\n * inviteId: pendingInviteId,\n * });\n * ```\n */\nexport const inviteRevoke = mutation({\n args: { inviteId: v.id(\"GroupInvite\") },\n returns: v.null(),\n handler: async (ctx, { inviteId }) => {\n const invite = await ctx.db.get(\"GroupInvite\", inviteId);\n if (invite === null) {\n throw new ConvexError({\n code: \"INVITE_NOT_FOUND\",\n message: \"Invite not found\",\n inviteId,\n });\n }\n if (invite.status !== \"pending\") {\n throw new ConvexError({\n code: \"INVITE_NOT_PENDING\",\n message: `Cannot revoke invite with status \"${invite.status}\"`,\n inviteId,\n currentStatus: invite.status,\n });\n }\n await ctx.db.patch(\"GroupInvite\", inviteId, { status: \"revoked\" });\n return null;\n },\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"members.d.ts","names":[],"sources":["../../../../src/component/public/groups/members.ts"],"mappings":";;;;;;;;;;;;;;;AAqCA;;;;;AAgDA;;;;;AAwCA;;;;;AAqGA;;;;;cA7La,SAAA;;;;;AAyUb;;;;;AA+BA;;;;;;;;;;cAxTa,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;cAqGA,uBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2DA,aAAA;;;;;;;;;;;;;;;;;;cAiFA,YAAA;;;;;;;;;;;;;;;;;;;;;;;cA+BA,YAAA"}
1
+ {"version":3,"file":"members.d.ts","names":[],"sources":["../../../../src/component/public/groups/members.ts"],"mappings":";;;;;;;;;;;;;;;AAsCA;;;;;AAgDA;;;;;AAwCA;;;;;AAqGA;;;;;cA7La,SAAA;;;;;AAyUb;;;;;AA+BA;;;;;;;;;;cAxTa,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;cAqGA,uBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2DA,aAAA;;;;;;;;;;;;;;;;;;cAiFA,YAAA;;;;;;;;;;;;;;;;;;;;;;;cA+BA,YAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"members.js","names":[],"sources":["../../../../src/component/public/groups/members.ts"],"sourcesContent":["import { ConvexError, v } from \"convex/values\";\nimport type { Id } from \"../../_generated/dataModel\";\nimport { mutation, query } from \"../../functions\";\nimport { vGroupMemberDoc, vPaginated } from \"../../model\";\n\n/**\n * Add a user as a member of a group.\n *\n * The `roleIds` field stores application-defined role identifiers. The auth\n * component stores assignments but does not enforce access control — your\n * application defines what each role means.\n *\n * Throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` when the user is\n * already a member of the target group. The duplicate check uses the\n * `group_id_user_id` compound index for an exact match.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to add the user to.\n * @param args.userId - The `Id<\"User\">` of the user to add as a member.\n * @param args.roleIds - Optional array of application-defined role identifiers (e.g. `[\"admin\", \"editor\"]`).\n * @param args.status - Optional membership status string (e.g. `\"active\"`, `\"suspended\"`). Defaults to whatever your application convention is.\n * @param args.extend - Optional arbitrary payload for application-specific metadata on the membership.\n * @returns The `Id<\"GroupMember\">` of the newly created member document.\n * @throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` if the user is already a member of this group.\n *\n * @example\n * ```ts\n * const memberId = await ctx.runMutation(\n * components.auth.groups.memberAdd,\n * {\n * groupId: teamGroupId,\n * userId: newUserId,\n * roleIds: [\"viewer\"],\n * status: \"active\",\n * },\n * );\n * ```\n */\nexport const memberAdd = mutation({\n args: {\n groupId: v.id(\"Group\"),\n userId: v.id(\"User\"),\n roleIds: v.optional(v.array(v.string())),\n status: v.optional(v.string()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"GroupMember\"),\n handler: async (ctx, args) => {\n const existingMembership = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", args.groupId).eq(\"userId\", args.userId),\n )\n .unique();\n if (existingMembership !== null) {\n throw new ConvexError({\n code: \"DUPLICATE_MEMBERSHIP\",\n message: \"User is already a member of this group\",\n groupId: args.groupId,\n userId: args.userId,\n existingMemberId: existingMembership._id,\n });\n }\n return await ctx.db.insert(\"GroupMember\", args);\n },\n});\n\n/**\n * Retrieve a member record by its document ID.\n *\n * Performs a direct lookup in the `GroupMember` table and returns the full\n * member document, or `null` if no member exists with the given ID.\n *\n * @param args.memberId - The `Id<\"GroupMember\">` of the member record to retrieve.\n * @returns The member document (including `groupId`, `userId`, `roleIds`, `status`, etc.) or `null` if not found.\n *\n * @example\n * ```ts\n * const member = await ctx.runQuery(components.auth.groups.memberGet, {\n * memberId: existingMemberId,\n * });\n * if (member !== null) {\n * console.log(member.userId, member.roleIds);\n * }\n * ```\n */\nexport const memberGet = query({\n args: { memberId: v.id(\"GroupMember\") },\n returns: v.union(vGroupMemberDoc, v.null()),\n handler: async (ctx, { memberId }) => {\n return await ctx.db.get(\"GroupMember\", memberId);\n },\n});\n\n/**\n * List members with optional filtering, sorting, and pagination.\n *\n * Returns `{ items, nextCursor }`. Supports filtering by `groupId`,\n * `userId`, `roleId`, and `status`. The query engine automatically selects\n * the best compound index based on the combination of filter fields\n * provided. The `roleId` filter is applied in-memory after the index scan\n * because role IDs are stored as an array.\n *\n * @param args.where - Optional filter criteria for narrowing results.\n * @param args.where.groupId - Match members belonging to this group.\n * @param args.where.userId - Match members for this specific user.\n * @param args.where.roleId - Match members whose `roleIds` array includes this role identifier.\n * @param args.where.status - Match members with this exact status string (e.g. `\"active\"`).\n * @param args.limit - Maximum number of items per page (clamped to 1..100, defaults to 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor` to fetch the next page, or `null` to start from the beginning.\n * @param args.orderBy - The field to sort by: `\"_creationTime\"` or `\"status\"`.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (defaults to `\"desc\"`).\n * @returns An object `{ items, nextCursor }` where `items` is an array of member documents and `nextCursor` is `null` when there are no more pages.\n *\n * @example\n * ```ts\n * const { items, nextCursor } = await ctx.runQuery(\n * components.auth.groups.memberList,\n * {\n * where: { groupId: teamGroupId, status: \"active\" },\n * limit: 30,\n * order: \"asc\",\n * },\n * );\n * ```\n */\nexport const memberList = query({\n args: {\n where: v.optional(\n v.object({\n groupId: v.optional(v.id(\"Group\")),\n userId: v.optional(v.id(\"User\")),\n roleId: v.optional(v.string()),\n status: v.optional(v.string()),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n orderBy: v.optional(\n v.union(v.literal(\"_creationTime\"), v.literal(\"status\")),\n ),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vGroupMemberDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n let q;\n if (where.groupId !== undefined && where.userId !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (idx) =>\n idx.eq(\"groupId\", where.groupId!).eq(\"userId\", where.userId!),\n );\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n } else if (where.groupId !== undefined && where.status !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_status\", (idx) =>\n idx.eq(\"groupId\", where.groupId!).eq(\"status\", where.status!),\n );\n } else if (where.groupId !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id\", (idx) => idx.eq(\"groupId\", where.groupId!));\n } else if (where.userId !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"user_id\", (idx) => idx.eq(\"userId\", where.userId!));\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n } else {\n q = ctx.db.query(\"GroupMember\");\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n }\n\n q = q.order(order);\n\n let all = await q.collect();\n if (where.roleId !== undefined) {\n all = all.filter((doc) => (doc.roleIds ?? []).includes(where.roleId!));\n }\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Look up a specific user's membership in a specific group.\n *\n * Uses the `group_id_user_id` compound index for an efficient exact-match\n * lookup. Returns `null` if the user is not a member of the group. Unlike\n * {@link memberResolve}, this does **not** walk the group hierarchy — it\n * checks only the specified group.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to check.\n * @param args.userId - The `Id<\"User\">` of the user whose membership to look up.\n * @returns The member document or `null` if the user is not a direct member of the group.\n *\n * @example\n * ```ts\n * const member = await ctx.runQuery(\n * components.auth.groups.memberGetByGroupAndUser,\n * { groupId: teamGroupId, userId: currentUserId },\n * );\n * if (member !== null) {\n * console.log(\"User has roles:\", member.roleIds);\n * }\n * ```\n */\nexport const memberGetByGroupAndUser = query({\n args: { groupId: v.id(\"Group\"), userId: v.id(\"User\") },\n returns: v.union(vGroupMemberDoc, v.null()),\n handler: async (ctx, { groupId, userId }) => {\n return await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", groupId).eq(\"userId\", userId),\n )\n .unique();\n },\n});\n\n/**\n * Resolve a user's membership by walking the group hierarchy from the\n * requested group up to the root. Returns the first matching membership\n * found, enabling inherited (ancestor-level) access checks.\n *\n * The traversal walks from `groupId` to its `parentGroupId`, then to the\n * parent's parent, and so on, up to `maxDepth` levels (default 32). It\n * stops at the first group where the user has a membership record. Cycle\n * detection prevents infinite loops if the hierarchy is malformed.\n *\n * When `ancestry` is `true`, the response includes a `traversedGroupIds`\n * array showing the full path that was walked (useful for debugging or\n * audit trails).\n *\n * This runs entirely inside the component (no cross-component RPCs per level).\n *\n * @param args.userId - The `Id<\"User\">` of the user whose membership to resolve.\n * @param args.groupId - The `Id<\"Group\">` to start the upward traversal from.\n * @param args.maxDepth - Optional maximum number of parent levels to traverse (defaults to 32). Set to `0` to check only the exact group.\n * @param args.ancestry - When `true`, the response includes the `traversedGroupIds` array showing all group IDs visited during the walk.\n * @returns An object with:\n * - `membership` — the member document at the matched group, or `null` if none was found.\n * - `matchedGroupId` — the ID of the group where membership was found, or `null`.\n * - `depth` — how many levels above `groupId` the match was found (0 = direct), or `null` if not found.\n * - `isDirect` — `true` when `depth === 0`.\n * - `isInherited` — `true` when `depth > 0`.\n * - `traversedGroupIds` — (only when `ancestry` is `true`) array of group IDs visited.\n *\n * @example\n * ```ts\n * const result = await ctx.runQuery(\n * components.auth.groups.memberResolve,\n * {\n * userId: currentUserId,\n * groupId: subTeamGroupId,\n * maxDepth: 5,\n * ancestry: true,\n * },\n * );\n * if (result.membership !== null) {\n * console.log(\n * result.isDirect ? \"Direct member\" : `Inherited from depth ${result.depth}`,\n * );\n * }\n * ```\n */\nexport const memberResolve = query({\n args: {\n userId: v.id(\"User\"),\n groupId: v.id(\"Group\"),\n maxDepth: v.optional(v.number()),\n ancestry: v.optional(v.boolean()),\n },\n returns: v.object({\n membership: v.union(vGroupMemberDoc, v.null()),\n matchedGroupId: v.union(v.id(\"Group\"), v.null()),\n depth: v.union(v.number(), v.null()),\n isDirect: v.boolean(),\n isInherited: v.boolean(),\n traversedGroupIds: v.optional(v.array(v.id(\"Group\"))),\n }),\n handler: async (ctx, args) => {\n const maxDepth = Math.max(0, Math.floor(args.maxDepth ?? 32));\n const includeAncestry = args.ancestry ?? false;\n const visited = new Set<string>();\n const traversedGroupIds: Id<\"Group\">[] = [];\n let currentGroupId: Id<\"Group\"> | undefined = args.groupId;\n let depth = 0;\n\n while (currentGroupId !== undefined && depth <= maxDepth) {\n if (visited.has(currentGroupId)) break;\n visited.add(currentGroupId);\n if (includeAncestry) traversedGroupIds.push(currentGroupId);\n\n const membership = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", currentGroupId!).eq(\"userId\", args.userId),\n )\n .unique();\n\n if (membership !== null) {\n return {\n membership,\n matchedGroupId: currentGroupId,\n depth,\n isDirect: depth === 0,\n isInherited: depth > 0,\n ...(includeAncestry ? { traversedGroupIds } : {}),\n };\n }\n\n const groupDoc: { parentGroupId?: Id<\"Group\"> } | null =\n await ctx.db.get(currentGroupId);\n if (!groupDoc?.parentGroupId) break;\n currentGroupId = groupDoc.parentGroupId;\n depth++;\n }\n\n return {\n membership: null,\n matchedGroupId: null,\n depth: null,\n isDirect: false,\n isInherited: false,\n ...(includeAncestry ? { traversedGroupIds } : {}),\n };\n },\n});\n\n/**\n * Remove a member from a group by permanently deleting the member record.\n *\n * This is a hard delete — the `GroupMember` document is removed from the\n * database entirely. If you need soft-delete semantics, use\n * {@link memberUpdate} to set the `status` field instead.\n *\n * @param args.memberId - The `Id<\"GroupMember\">` of the member record to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.memberRemove, {\n * memberId: memberToRemoveId,\n * });\n * ```\n */\nexport const memberRemove = mutation({\n args: { memberId: v.id(\"GroupMember\") },\n returns: v.null(),\n handler: async (ctx, { memberId }) => {\n await ctx.db.delete(\"GroupMember\", memberId);\n return null;\n },\n});\n\n/**\n * Update a member record's mutable fields such as `roleIds`, `status`, and\n * `extend`.\n *\n * Uses `db.patch` under the hood, so only the fields present in `data` are\n * modified — all other fields on the member document are left unchanged.\n *\n * @param args.memberId - The `Id<\"GroupMember\">` of the member record to update.\n * @param args.data - A partial object of fields to patch. Supported keys include `roleIds`, `status`, and `extend`.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.memberUpdate, {\n * memberId: existingMemberId,\n * data: {\n * roleIds: [\"admin\", \"editor\"],\n * status: \"active\",\n * },\n * });\n * ```\n */\nexport const memberUpdate = mutation({\n args: { memberId: v.id(\"GroupMember\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { memberId, data }) => {\n await ctx.db.patch(\"GroupMember\", memberId, data);\n return null;\n },\n});\n\n// ============================================================================\n// Invites\n// ============================================================================\n"],"mappings":"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"}
1
+ {"version":3,"file":"members.js","names":[],"sources":["../../../../src/component/public/groups/members.ts"],"sourcesContent":["import { ConvexError, v } from \"convex/values\";\n\nimport type { Id } from \"../../_generated/dataModel\";\nimport { mutation, query } from \"../../functions\";\nimport { vGroupMemberDoc, vPaginated } from \"../../model\";\n\n/**\n * Add a user as a member of a group.\n *\n * The `roleIds` field stores application-defined role identifiers. The auth\n * component stores assignments but does not enforce access control — your\n * application defines what each role means.\n *\n * Throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` when the user is\n * already a member of the target group. The duplicate check uses the\n * `group_id_user_id` compound index for an exact match.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to add the user to.\n * @param args.userId - The `Id<\"User\">` of the user to add as a member.\n * @param args.roleIds - Optional array of application-defined role identifiers (e.g. `[\"admin\", \"editor\"]`).\n * @param args.status - Optional membership status string (e.g. `\"active\"`, `\"suspended\"`). Defaults to whatever your application convention is.\n * @param args.extend - Optional arbitrary payload for application-specific metadata on the membership.\n * @returns The `Id<\"GroupMember\">` of the newly created member document.\n * @throws `ConvexError` with code `DUPLICATE_MEMBERSHIP` if the user is already a member of this group.\n *\n * @example\n * ```ts\n * const memberId = await ctx.runMutation(\n * components.auth.groups.memberAdd,\n * {\n * groupId: teamGroupId,\n * userId: newUserId,\n * roleIds: [\"viewer\"],\n * status: \"active\",\n * },\n * );\n * ```\n */\nexport const memberAdd = mutation({\n args: {\n groupId: v.id(\"Group\"),\n userId: v.id(\"User\"),\n roleIds: v.optional(v.array(v.string())),\n status: v.optional(v.string()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"GroupMember\"),\n handler: async (ctx, args) => {\n const existingMembership = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", args.groupId).eq(\"userId\", args.userId),\n )\n .unique();\n if (existingMembership !== null) {\n throw new ConvexError({\n code: \"DUPLICATE_MEMBERSHIP\",\n message: \"User is already a member of this group\",\n groupId: args.groupId,\n userId: args.userId,\n existingMemberId: existingMembership._id,\n });\n }\n return await ctx.db.insert(\"GroupMember\", args);\n },\n});\n\n/**\n * Retrieve a member record by its document ID.\n *\n * Performs a direct lookup in the `GroupMember` table and returns the full\n * member document, or `null` if no member exists with the given ID.\n *\n * @param args.memberId - The `Id<\"GroupMember\">` of the member record to retrieve.\n * @returns The member document (including `groupId`, `userId`, `roleIds`, `status`, etc.) or `null` if not found.\n *\n * @example\n * ```ts\n * const member = await ctx.runQuery(components.auth.groups.memberGet, {\n * memberId: existingMemberId,\n * });\n * if (member !== null) {\n * console.log(member.userId, member.roleIds);\n * }\n * ```\n */\nexport const memberGet = query({\n args: { memberId: v.id(\"GroupMember\") },\n returns: v.union(vGroupMemberDoc, v.null()),\n handler: async (ctx, { memberId }) => {\n return await ctx.db.get(\"GroupMember\", memberId);\n },\n});\n\n/**\n * List members with optional filtering, sorting, and pagination.\n *\n * Returns `{ items, nextCursor }`. Supports filtering by `groupId`,\n * `userId`, `roleId`, and `status`. The query engine automatically selects\n * the best compound index based on the combination of filter fields\n * provided. The `roleId` filter is applied in-memory after the index scan\n * because role IDs are stored as an array.\n *\n * @param args.where - Optional filter criteria for narrowing results.\n * @param args.where.groupId - Match members belonging to this group.\n * @param args.where.userId - Match members for this specific user.\n * @param args.where.roleId - Match members whose `roleIds` array includes this role identifier.\n * @param args.where.status - Match members with this exact status string (e.g. `\"active\"`).\n * @param args.limit - Maximum number of items per page (clamped to 1..100, defaults to 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor` to fetch the next page, or `null` to start from the beginning.\n * @param args.orderBy - The field to sort by: `\"_creationTime\"` or `\"status\"`.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (defaults to `\"desc\"`).\n * @returns An object `{ items, nextCursor }` where `items` is an array of member documents and `nextCursor` is `null` when there are no more pages.\n *\n * @example\n * ```ts\n * const { items, nextCursor } = await ctx.runQuery(\n * components.auth.groups.memberList,\n * {\n * where: { groupId: teamGroupId, status: \"active\" },\n * limit: 30,\n * order: \"asc\",\n * },\n * );\n * ```\n */\nexport const memberList = query({\n args: {\n where: v.optional(\n v.object({\n groupId: v.optional(v.id(\"Group\")),\n userId: v.optional(v.id(\"User\")),\n roleId: v.optional(v.string()),\n status: v.optional(v.string()),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n orderBy: v.optional(\n v.union(v.literal(\"_creationTime\"), v.literal(\"status\")),\n ),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vGroupMemberDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n let q;\n if (where.groupId !== undefined && where.userId !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (idx) =>\n idx.eq(\"groupId\", where.groupId!).eq(\"userId\", where.userId!),\n );\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n } else if (where.groupId !== undefined && where.status !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_status\", (idx) =>\n idx.eq(\"groupId\", where.groupId!).eq(\"status\", where.status!),\n );\n } else if (where.groupId !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id\", (idx) => idx.eq(\"groupId\", where.groupId!));\n } else if (where.userId !== undefined) {\n q = ctx.db\n .query(\"GroupMember\")\n .withIndex(\"user_id\", (idx) => idx.eq(\"userId\", where.userId!));\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n } else {\n q = ctx.db.query(\"GroupMember\");\n if (where.status !== undefined) {\n q = q.filter((f) => f.eq(f.field(\"status\"), where.status!));\n }\n }\n\n q = q.order(order);\n\n let all = await q.collect();\n if (where.roleId !== undefined) {\n all = all.filter((doc) => (doc.roleIds ?? []).includes(where.roleId!));\n }\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Look up a specific user's membership in a specific group.\n *\n * Uses the `group_id_user_id` compound index for an efficient exact-match\n * lookup. Returns `null` if the user is not a member of the group. Unlike\n * {@link memberResolve}, this does **not** walk the group hierarchy — it\n * checks only the specified group.\n *\n * @param args.groupId - The `Id<\"Group\">` of the group to check.\n * @param args.userId - The `Id<\"User\">` of the user whose membership to look up.\n * @returns The member document or `null` if the user is not a direct member of the group.\n *\n * @example\n * ```ts\n * const member = await ctx.runQuery(\n * components.auth.groups.memberGetByGroupAndUser,\n * { groupId: teamGroupId, userId: currentUserId },\n * );\n * if (member !== null) {\n * console.log(\"User has roles:\", member.roleIds);\n * }\n * ```\n */\nexport const memberGetByGroupAndUser = query({\n args: { groupId: v.id(\"Group\"), userId: v.id(\"User\") },\n returns: v.union(vGroupMemberDoc, v.null()),\n handler: async (ctx, { groupId, userId }) => {\n return await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", groupId).eq(\"userId\", userId),\n )\n .unique();\n },\n});\n\n/**\n * Resolve a user's membership by walking the group hierarchy from the\n * requested group up to the root. Returns the first matching membership\n * found, enabling inherited (ancestor-level) access checks.\n *\n * The traversal walks from `groupId` to its `parentGroupId`, then to the\n * parent's parent, and so on, up to `maxDepth` levels (default 32). It\n * stops at the first group where the user has a membership record. Cycle\n * detection prevents infinite loops if the hierarchy is malformed.\n *\n * When `ancestry` is `true`, the response includes a `traversedGroupIds`\n * array showing the full path that was walked (useful for debugging or\n * audit trails).\n *\n * This runs entirely inside the component (no cross-component RPCs per level).\n *\n * @param args.userId - The `Id<\"User\">` of the user whose membership to resolve.\n * @param args.groupId - The `Id<\"Group\">` to start the upward traversal from.\n * @param args.maxDepth - Optional maximum number of parent levels to traverse (defaults to 32). Set to `0` to check only the exact group.\n * @param args.ancestry - When `true`, the response includes the `traversedGroupIds` array showing all group IDs visited during the walk.\n * @returns An object with:\n * - `membership` — the member document at the matched group, or `null` if none was found.\n * - `matchedGroupId` — the ID of the group where membership was found, or `null`.\n * - `depth` — how many levels above `groupId` the match was found (0 = direct), or `null` if not found.\n * - `isDirect` — `true` when `depth === 0`.\n * - `isInherited` — `true` when `depth > 0`.\n * - `traversedGroupIds` — (only when `ancestry` is `true`) array of group IDs visited.\n *\n * @example\n * ```ts\n * const result = await ctx.runQuery(\n * components.auth.groups.memberResolve,\n * {\n * userId: currentUserId,\n * groupId: subTeamGroupId,\n * maxDepth: 5,\n * ancestry: true,\n * },\n * );\n * if (result.membership !== null) {\n * console.log(\n * result.isDirect ? \"Direct member\" : `Inherited from depth ${result.depth}`,\n * );\n * }\n * ```\n */\nexport const memberResolve = query({\n args: {\n userId: v.id(\"User\"),\n groupId: v.id(\"Group\"),\n maxDepth: v.optional(v.number()),\n ancestry: v.optional(v.boolean()),\n },\n returns: v.object({\n membership: v.union(vGroupMemberDoc, v.null()),\n matchedGroupId: v.union(v.id(\"Group\"), v.null()),\n depth: v.union(v.number(), v.null()),\n isDirect: v.boolean(),\n isInherited: v.boolean(),\n traversedGroupIds: v.optional(v.array(v.id(\"Group\"))),\n }),\n handler: async (ctx, args) => {\n const maxDepth = Math.max(0, Math.floor(args.maxDepth ?? 32));\n const includeAncestry = args.ancestry ?? false;\n const visited = new Set<string>();\n const traversedGroupIds: Id<\"Group\">[] = [];\n let currentGroupId: Id<\"Group\"> | undefined = args.groupId;\n let depth = 0;\n\n while (currentGroupId !== undefined && depth <= maxDepth) {\n if (visited.has(currentGroupId)) break;\n visited.add(currentGroupId);\n if (includeAncestry) traversedGroupIds.push(currentGroupId);\n\n const membership = await ctx.db\n .query(\"GroupMember\")\n .withIndex(\"group_id_user_id\", (q) =>\n q.eq(\"groupId\", currentGroupId!).eq(\"userId\", args.userId),\n )\n .unique();\n\n if (membership !== null) {\n return {\n membership,\n matchedGroupId: currentGroupId,\n depth,\n isDirect: depth === 0,\n isInherited: depth > 0,\n ...(includeAncestry ? { traversedGroupIds } : {}),\n };\n }\n\n const groupDoc: { parentGroupId?: Id<\"Group\"> } | null =\n await ctx.db.get(currentGroupId);\n if (!groupDoc?.parentGroupId) break;\n currentGroupId = groupDoc.parentGroupId;\n depth++;\n }\n\n return {\n membership: null,\n matchedGroupId: null,\n depth: null,\n isDirect: false,\n isInherited: false,\n ...(includeAncestry ? { traversedGroupIds } : {}),\n };\n },\n});\n\n/**\n * Remove a member from a group by permanently deleting the member record.\n *\n * This is a hard delete — the `GroupMember` document is removed from the\n * database entirely. If you need soft-delete semantics, use\n * {@link memberUpdate} to set the `status` field instead.\n *\n * @param args.memberId - The `Id<\"GroupMember\">` of the member record to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.memberRemove, {\n * memberId: memberToRemoveId,\n * });\n * ```\n */\nexport const memberRemove = mutation({\n args: { memberId: v.id(\"GroupMember\") },\n returns: v.null(),\n handler: async (ctx, { memberId }) => {\n await ctx.db.delete(\"GroupMember\", memberId);\n return null;\n },\n});\n\n/**\n * Update a member record's mutable fields such as `roleIds`, `status`, and\n * `extend`.\n *\n * Uses `db.patch` under the hood, so only the fields present in `data` are\n * modified — all other fields on the member document are left unchanged.\n *\n * @param args.memberId - The `Id<\"GroupMember\">` of the member record to update.\n * @param args.data - A partial object of fields to patch. Supported keys include `roleIds`, `status`, and `extend`.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(components.auth.groups.memberUpdate, {\n * memberId: existingMemberId,\n * data: {\n * roleIds: [\"admin\", \"editor\"],\n * status: \"active\",\n * },\n * });\n * ```\n */\nexport const memberUpdate = mutation({\n args: { memberId: v.id(\"GroupMember\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { memberId, data }) => {\n await ctx.db.patch(\"GroupMember\", memberId, data);\n return null;\n },\n});\n\n// ============================================================================\n// Invites\n// ============================================================================\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"accounts.d.ts","names":[],"sources":["../../../../src/component/public/identity/accounts.ts"],"mappings":";;;;;;;;;;;;;;AA0BA;;;;;AAmCA;;;;;AAiCA;cApEa,iBAAA;;;;AAwGb;;;;;AAoCA;;;;;AA4BA;;;;;;;;;;;cArIa,UAAA;;;;;;;;;;;;;;;;;;;;;cAiCA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,aAAA;;;;;;;;;;;;;;;;;;;;;;;cAoCA,YAAA;;;;;;;;;;;;;;;;;;;;cA4BA,aAAA"}
1
+ {"version":3,"file":"accounts.d.ts","names":[],"sources":["../../../../src/component/public/identity/accounts.ts"],"mappings":";;;;;;;;;;;;;;AA2BA;;;;;AAmCA;;;;;AAiCA;cApEa,iBAAA;;;;AAwGb;;;;;AAoCA;;;;;AA4BA;;;;;;;;;;;cArIa,UAAA;;;;;;;;;;;;;;;;;;;;;cAiCA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,aAAA;;;;;;;;;;;;;;;;;;;;;;;cAoCA,YAAA;;;;;;;;;;;;;;;;;;;;cA4BA,aAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"accounts.js","names":[],"sources":["../../../../src/component/public/identity/accounts.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vAccountDoc } from \"../../model\";\n\n/**\n * List all accounts linked to a specific user.\n *\n * Queries the `Account` table using the `user_id_provider` index to efficiently\n * retrieve every authentication account (e.g. OAuth, credentials, email) that\n * belongs to the given user.\n *\n * @param args.userId - The document ID of the user whose accounts should be retrieved.\n * @returns An array of account documents associated with the user. Each document\n * includes fields such as `provider`, `providerAccountId`, `secret`, and `extend`.\n *\n * @example\n * ```ts\n * const accounts = await ctx.runQuery(\n * component.identity.accounts.accountListByUser,\n * { userId: user._id },\n * );\n * for (const account of accounts) {\n * console.log(`Provider: ${account.provider}, ID: ${account.providerAccountId}`);\n * }\n * ```\n */\nexport const accountListByUser = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vAccountDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"Account\")\n .withIndex(\"user_id_provider\", (q) => q.eq(\"userId\", userId as any))\n .collect();\n },\n});\n\n/**\n * Look up an account by its provider name and provider-specific account ID.\n *\n * Uses the `provider_account_id` index to find the unique account that matches\n * the given provider and external account identifier. This is the primary way\n * to resolve an incoming authentication event (e.g. an OAuth callback) to an\n * existing account in the system.\n *\n * @param args.provider - The name of the authentication provider (e.g. `\"google\"`, `\"github\"`, `\"credentials\"`).\n * @param args.providerAccountId - The unique identifier assigned to the user by the external provider.\n * @returns The matching account document, or `null` if no account exists for the\n * given provider and provider account ID combination.\n *\n * @example\n * ```ts\n * const account = await ctx.runQuery(\n * component.identity.accounts.accountGet,\n * { provider: \"google\", providerAccountId: \"1184210396400123\" },\n * );\n * if (account !== null) {\n * console.log(`Found account for user: ${account.userId}`);\n * }\n * ```\n */\nexport const accountGet = query({\n args: { provider: v.string(), providerAccountId: v.string() },\n returns: v.union(vAccountDoc, v.null()),\n handler: async (ctx, { provider, providerAccountId }) => {\n return await ctx.db\n .query(\"Account\")\n .withIndex(\"provider_account_id\", (q) =>\n q.eq(\"provider\", provider).eq(\"providerAccountId\", providerAccountId),\n )\n .unique();\n },\n});\n\n/**\n * Retrieve a single account by its Convex document ID.\n *\n * Performs a direct point lookup on the `Account` table. Returns `null` if the\n * document has been deleted or never existed.\n *\n * @param args.accountId - The Convex document ID (`Id<\"Account\">`) of the account to retrieve.\n * @returns The account document if it exists, or `null` otherwise.\n *\n * @example\n * ```ts\n * const account = await ctx.runQuery(\n * component.identity.accounts.accountGetById,\n * { accountId: existingAccountId },\n * );\n * if (account !== null) {\n * console.log(`Provider: ${account.provider}`);\n * }\n * ```\n */\nexport const accountGetById = query({\n args: { accountId: v.id(\"Account\") },\n returns: v.union(vAccountDoc, v.null()),\n handler: async (ctx, { accountId }) => {\n return await ctx.db.get(\"Account\", accountId);\n },\n});\n\n/**\n * Create a new account that links a user to an authentication provider.\n *\n * Inserts a row into the `Account` table, establishing the relationship between\n * a user document and an external authentication provider (OAuth, credentials,\n * email/phone OTP, etc.). A single user may have multiple accounts for different\n * providers.\n *\n * @param args.userId - The document ID of the user to link this account to.\n * @param args.provider - The name of the authentication provider (e.g. `\"google\"`, `\"credentials\"`).\n * @param args.providerAccountId - The unique identifier for this user within the external provider.\n * @param args.secret - An optional hashed secret (e.g. password hash) stored for credential-based providers.\n * @param args.extend - Optional arbitrary data to store alongside the account for application-specific needs.\n * @returns The document ID of the newly created account.\n *\n * @example\n * ```ts\n * const accountId = await ctx.runMutation(\n * component.identity.accounts.accountInsert,\n * {\n * userId: user._id,\n * provider: \"credentials\",\n * providerAccountId: \"user@example.com\",\n * secret: hashedPassword,\n * },\n * );\n * ```\n */\nexport const accountInsert = mutation({\n args: {\n userId: v.id(\"User\"),\n provider: v.string(),\n providerAccountId: v.string(),\n secret: v.optional(v.string()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"Account\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"Account\", args as any);\n },\n});\n\n/**\n * Patch an existing account document with partial data.\n *\n * Merges the provided fields into the existing account document. Fields not\n * included in `data` are left unchanged. This is useful for updating a stored\n * secret (e.g. after a password change) or modifying extended metadata.\n *\n * @param args.accountId - The document ID of the account to update.\n * @param args.data - A partial object containing the fields to merge into the account document.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * component.identity.accounts.accountPatch,\n * {\n * accountId: account._id,\n * data: { secret: newHashedPassword },\n * },\n * );\n * ```\n */\nexport const accountPatch = mutation({\n args: { accountId: v.id(\"Account\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { accountId, data }) => {\n await ctx.db.patch(\"Account\", accountId, data);\n return null;\n },\n});\n\n/**\n * Delete an account document permanently.\n *\n * Removes the account from the `Account` table. This effectively unlinks the\n * user from the corresponding authentication provider. Callers should ensure\n * that related resources (verification codes, sessions, etc.) are cleaned up\n * separately if needed.\n *\n * @param args.accountId - The document ID of the account to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * component.identity.accounts.accountDelete,\n * { accountId: account._id },\n * );\n * ```\n */\nexport const accountDelete = mutation({\n args: { accountId: v.id(\"Account\") },\n returns: v.null(),\n handler: async (ctx, { accountId }) => {\n await ctx.db.delete(\"Account\", accountId);\n return null;\n },\n});\n\n// ============================================================================\n// Sessions\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,oBAAoB,MAAM;CACrC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,YAAY;CAC7B,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,qBAAqB,MAAM,EAAE,GAAG,UAAU,OAAc,CAAC,CACnE,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,aAAa,MAAM;CAC9B,MAAM;EAAE,UAAU,EAAE,QAAQ;EAAE,mBAAmB,EAAE,QAAQ;EAAE;CAC7D,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,UAAU,wBAAwB;AACvD,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,wBAAwB,MACjC,EAAE,GAAG,YAAY,SAAS,CAAC,GAAG,qBAAqB,kBAAkB,CACtE,CACA,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,iBAAiB,MAAM;CAClC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GAAG,IAAI,WAAW,UAAU;;CAEhD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BF,MAAa,gBAAgB,SAAS;CACpC,MAAM;EACJ,QAAQ,EAAE,GAAG,OAAO;EACpB,UAAU,EAAE,QAAQ;EACpB,mBAAmB,EAAE,QAAQ;EAC7B,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC9B,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC;EAC5B;CACD,SAAS,EAAE,GAAG,UAAU;CACxB,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,WAAW,KAAY;;CAErD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,eAAe,SAAS;CACnC,MAAM;EAAE,WAAW,EAAE,GAAG,UAAU;EAAE,MAAM,EAAE,KAAK;EAAE;CACnD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,WAAW,WAAW;AAC3C,QAAM,IAAI,GAAG,MAAM,WAAW,WAAW,KAAK;AAC9C,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,gBAAgB,SAAS;CACpC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,QAAM,IAAI,GAAG,OAAO,WAAW,UAAU;AACzC,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"accounts.js","names":[],"sources":["../../../../src/component/public/identity/accounts.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vAccountDoc } from \"../../model\";\n\n/**\n * List all accounts linked to a specific user.\n *\n * Queries the `Account` table using the `user_id_provider` index to efficiently\n * retrieve every authentication account (e.g. OAuth, credentials, email) that\n * belongs to the given user.\n *\n * @param args.userId - The document ID of the user whose accounts should be retrieved.\n * @returns An array of account documents associated with the user. Each document\n * includes fields such as `provider`, `providerAccountId`, `secret`, and `extend`.\n *\n * @example\n * ```ts\n * const accounts = await ctx.runQuery(\n * component.identity.accounts.accountListByUser,\n * { userId: user._id },\n * );\n * for (const account of accounts) {\n * console.log(`Provider: ${account.provider}, ID: ${account.providerAccountId}`);\n * }\n * ```\n */\nexport const accountListByUser = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vAccountDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"Account\")\n .withIndex(\"user_id_provider\", (q) => q.eq(\"userId\", userId as any))\n .collect();\n },\n});\n\n/**\n * Look up an account by its provider name and provider-specific account ID.\n *\n * Uses the `provider_account_id` index to find the unique account that matches\n * the given provider and external account identifier. This is the primary way\n * to resolve an incoming authentication event (e.g. an OAuth callback) to an\n * existing account in the system.\n *\n * @param args.provider - The name of the authentication provider (e.g. `\"google\"`, `\"github\"`, `\"credentials\"`).\n * @param args.providerAccountId - The unique identifier assigned to the user by the external provider.\n * @returns The matching account document, or `null` if no account exists for the\n * given provider and provider account ID combination.\n *\n * @example\n * ```ts\n * const account = await ctx.runQuery(\n * component.identity.accounts.accountGet,\n * { provider: \"google\", providerAccountId: \"1184210396400123\" },\n * );\n * if (account !== null) {\n * console.log(`Found account for user: ${account.userId}`);\n * }\n * ```\n */\nexport const accountGet = query({\n args: { provider: v.string(), providerAccountId: v.string() },\n returns: v.union(vAccountDoc, v.null()),\n handler: async (ctx, { provider, providerAccountId }) => {\n return await ctx.db\n .query(\"Account\")\n .withIndex(\"provider_account_id\", (q) =>\n q.eq(\"provider\", provider).eq(\"providerAccountId\", providerAccountId),\n )\n .unique();\n },\n});\n\n/**\n * Retrieve a single account by its Convex document ID.\n *\n * Performs a direct point lookup on the `Account` table. Returns `null` if the\n * document has been deleted or never existed.\n *\n * @param args.accountId - The Convex document ID (`Id<\"Account\">`) of the account to retrieve.\n * @returns The account document if it exists, or `null` otherwise.\n *\n * @example\n * ```ts\n * const account = await ctx.runQuery(\n * component.identity.accounts.accountGetById,\n * { accountId: existingAccountId },\n * );\n * if (account !== null) {\n * console.log(`Provider: ${account.provider}`);\n * }\n * ```\n */\nexport const accountGetById = query({\n args: { accountId: v.id(\"Account\") },\n returns: v.union(vAccountDoc, v.null()),\n handler: async (ctx, { accountId }) => {\n return await ctx.db.get(\"Account\", accountId);\n },\n});\n\n/**\n * Create a new account that links a user to an authentication provider.\n *\n * Inserts a row into the `Account` table, establishing the relationship between\n * a user document and an external authentication provider (OAuth, credentials,\n * email/phone OTP, etc.). A single user may have multiple accounts for different\n * providers.\n *\n * @param args.userId - The document ID of the user to link this account to.\n * @param args.provider - The name of the authentication provider (e.g. `\"google\"`, `\"credentials\"`).\n * @param args.providerAccountId - The unique identifier for this user within the external provider.\n * @param args.secret - An optional hashed secret (e.g. password hash) stored for credential-based providers.\n * @param args.extend - Optional arbitrary data to store alongside the account for application-specific needs.\n * @returns The document ID of the newly created account.\n *\n * @example\n * ```ts\n * const accountId = await ctx.runMutation(\n * component.identity.accounts.accountInsert,\n * {\n * userId: user._id,\n * provider: \"credentials\",\n * providerAccountId: \"user@example.com\",\n * secret: hashedPassword,\n * },\n * );\n * ```\n */\nexport const accountInsert = mutation({\n args: {\n userId: v.id(\"User\"),\n provider: v.string(),\n providerAccountId: v.string(),\n secret: v.optional(v.string()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"Account\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"Account\", args as any);\n },\n});\n\n/**\n * Patch an existing account document with partial data.\n *\n * Merges the provided fields into the existing account document. Fields not\n * included in `data` are left unchanged. This is useful for updating a stored\n * secret (e.g. after a password change) or modifying extended metadata.\n *\n * @param args.accountId - The document ID of the account to update.\n * @param args.data - A partial object containing the fields to merge into the account document.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * component.identity.accounts.accountPatch,\n * {\n * accountId: account._id,\n * data: { secret: newHashedPassword },\n * },\n * );\n * ```\n */\nexport const accountPatch = mutation({\n args: { accountId: v.id(\"Account\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { accountId, data }) => {\n await ctx.db.patch(\"Account\", accountId, data);\n return null;\n },\n});\n\n/**\n * Delete an account document permanently.\n *\n * Removes the account from the `Account` table. This effectively unlinks the\n * user from the corresponding authentication provider. Callers should ensure\n * that related resources (verification codes, sessions, etc.) are cleaned up\n * separately if needed.\n *\n * @param args.accountId - The document ID of the account to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * component.identity.accounts.accountDelete,\n * { accountId: account._id },\n * );\n * ```\n */\nexport const accountDelete = mutation({\n args: { accountId: v.id(\"Account\") },\n returns: v.null(),\n handler: async (ctx, { accountId }) => {\n await ctx.db.delete(\"Account\", accountId);\n return null;\n },\n});\n\n// ============================================================================\n// Sessions\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,oBAAoB,MAAM;CACrC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,YAAY;CAC7B,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,qBAAqB,MAAM,EAAE,GAAG,UAAU,OAAc,CAAC,CACnE,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,aAAa,MAAM;CAC9B,MAAM;EAAE,UAAU,EAAE,QAAQ;EAAE,mBAAmB,EAAE,QAAQ;EAAE;CAC7D,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,UAAU,wBAAwB;AACvD,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,wBAAwB,MACjC,EAAE,GAAG,YAAY,SAAS,CAAC,GAAG,qBAAqB,kBAAkB,CACtE,CACA,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,iBAAiB,MAAM;CAClC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GAAG,IAAI,WAAW,UAAU;;CAEhD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BF,MAAa,gBAAgB,SAAS;CACpC,MAAM;EACJ,QAAQ,EAAE,GAAG,OAAO;EACpB,UAAU,EAAE,QAAQ;EACpB,mBAAmB,EAAE,QAAQ;EAC7B,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC9B,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC;EAC5B;CACD,SAAS,EAAE,GAAG,UAAU;CACxB,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,WAAW,KAAY;;CAErD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,eAAe,SAAS;CACnC,MAAM;EAAE,WAAW,EAAE,GAAG,UAAU;EAAE,MAAM,EAAE,KAAK;EAAE;CACnD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,WAAW,WAAW;AAC3C,QAAM,IAAI,GAAG,MAAM,WAAW,WAAW,KAAK;AAC9C,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,gBAAgB,SAAS;CACpC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,QAAM,IAAI,GAAG,OAAO,WAAW,UAAU;AACzC,SAAO;;CAEV,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"codes.d.ts","names":[],"sources":["../../../../src/component/public/identity/codes.ts"],"mappings":";;;;;;;;;;;;AAyBA;;;;;AAgCA;;;;;AA4CA;;cA5Ea,8BAAA;;;AA+Gb;;;;;;;;;;;;;;;;;;;cA/Ea,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4CA,sBAAA;;;;;;;;;;;;;;;;;;;;cAmCA,sBAAA"}
1
+ {"version":3,"file":"codes.d.ts","names":[],"sources":["../../../../src/component/public/identity/codes.ts"],"mappings":";;;;;;;;;;;;AA0BA;;;;;AAgCA;;;;;AA4CA;;cA5Ea,8BAAA;;;AA+Gb;;;;;;;;;;;;;;;;;;;cA/Ea,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4CA,sBAAA;;;;;;;;;;;;;;;;;;;;cAmCA,sBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"codes.js","names":[],"sources":["../../../../src/component/public/identity/codes.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vVerificationCodeDoc } from \"../../model\";\n\n/**\n * Find a verification code by its associated account ID.\n *\n * Queries the `VerificationCode` table using the `account_id` index to locate\n * the unique verification code linked to the given account. Each account has at\n * most one active verification code at a time.\n *\n * @param args.accountId - The document ID of the account whose verification code should be retrieved.\n * @returns The verification code document if one exists for the account, or `null` otherwise.\n *\n * @example\n * ```ts\n * const code = await ctx.runQuery(\n * component.identity.codes.verificationCodeGetByAccountId,\n * { accountId: account._id },\n * );\n * if (code !== null && code.expirationTime > Date.now()) {\n * console.log(\"Active verification code exists\");\n * }\n * ```\n */\nexport const verificationCodeGetByAccountId = query({\n args: { accountId: v.id(\"Account\") },\n returns: v.union(vVerificationCodeDoc, v.null()),\n handler: async (ctx, { accountId }) => {\n return await ctx.db\n .query(\"VerificationCode\")\n .withIndex(\"account_id\", (q) => q.eq(\"accountId\", accountId as any))\n .unique();\n },\n});\n\n/**\n * Find a verification code by its code string value.\n *\n * Queries the `VerificationCode` table using the `code` index to locate the\n * unique verification code document matching the given code string. This is\n * the primary lookup used when a user submits an OTP or clicks a magic link.\n *\n * @param args.code - The verification code string to look up (e.g. a 6-digit OTP or a magic-link token).\n * @returns The verification code document if a match is found, or `null` otherwise.\n *\n * @example\n * ```ts\n * const codeDoc = await ctx.runQuery(\n * component.identity.codes.verificationCodeGetByCode,\n * { code: \"482910\" },\n * );\n * if (codeDoc !== null && codeDoc.expirationTime > Date.now()) {\n * console.log(`Code is valid for account: ${codeDoc.accountId}`);\n * }\n * ```\n */\nexport const verificationCodeGetByCode = query({\n args: { code: v.string() },\n returns: v.union(vVerificationCodeDoc, v.null()),\n handler: async (ctx, { code }) => {\n return await ctx.db\n .query(\"VerificationCode\")\n .withIndex(\"code\", (q) => q.eq(\"code\", code))\n .unique();\n },\n});\n\n/**\n * Create a new verification code for OTP, magic link, or OAuth flows.\n *\n * Inserts a document into the `VerificationCode` table that ties a short-lived\n * code to a specific account and provider. The code can be used for email OTP,\n * phone OTP, magic link, or OAuth state verification depending on the flow.\n *\n * @param args.accountId - The document ID of the account this verification code is associated with.\n * @param args.provider - The name of the authentication provider initiating the verification\n * (e.g. `\"resend-otp\"`, `\"twilio-otp\"`, `\"google\"`).\n * @param args.code - The verification code string (e.g. a random OTP or an opaque token for magic links).\n * @param args.expirationTime - The Unix timestamp (in milliseconds) at which this code expires.\n * @param args.verifier - An optional PKCE verifier string used in OAuth/OIDC flows to prevent CSRF attacks.\n * @param args.emailVerified - An optional email address that will be marked as verified upon successful\n * code redemption.\n * @param args.phoneVerified - An optional phone number that will be marked as verified upon successful\n * code redemption.\n * @returns The document ID of the newly created verification code.\n *\n * @example\n * ```ts\n * const codeId = await ctx.runMutation(\n * component.identity.codes.verificationCodeCreate,\n * {\n * accountId: account._id,\n * provider: \"resend-otp\",\n * code: \"482910\",\n * expirationTime: Date.now() + 10 * 60 * 1000, // 10 minutes\n * emailVerified: \"alice@example.com\",\n * },\n * );\n * ```\n */\nexport const verificationCodeCreate = mutation({\n args: {\n accountId: v.id(\"Account\"),\n provider: v.string(),\n code: v.string(),\n expirationTime: v.number(),\n verifier: v.optional(v.string()),\n emailVerified: v.optional(v.string()),\n phoneVerified: v.optional(v.string()),\n },\n returns: v.id(\"VerificationCode\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"VerificationCode\", args as any);\n },\n});\n\n/**\n * Delete a verification code document permanently.\n *\n * Removes the verification code from the `VerificationCode` table. This is\n * typically called after the code has been successfully redeemed or when it\n * needs to be invalidated (e.g. replaced by a new code).\n *\n * @param args.verificationCodeId - The document ID of the verification code to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // Delete the code after successful verification\n * await ctx.runMutation(\n * component.identity.codes.verificationCodeDelete,\n * { verificationCodeId: codeDoc._id },\n * );\n * ```\n */\nexport const verificationCodeDelete = mutation({\n args: { verificationCodeId: v.id(\"VerificationCode\") },\n returns: v.null(),\n handler: async (ctx, { verificationCodeId }) => {\n await ctx.db.delete(\"VerificationCode\", verificationCodeId);\n return null;\n },\n});\n\n// ============================================================================\n// Refresh Tokens\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,iCAAiC,MAAM;CAClD,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM,sBAAsB,EAAE,MAAM,CAAC;CAChD,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GACd,MAAM,mBAAmB,CACzB,UAAU,eAAe,MAAM,EAAE,GAAG,aAAa,UAAiB,CAAC,CACnE,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBF,MAAa,4BAA4B,MAAM;CAC7C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE;CAC1B,SAAS,EAAE,MAAM,sBAAsB,EAAE,MAAM,CAAC;CAChD,SAAS,OAAO,KAAK,EAAE,WAAW;AAChC,SAAO,MAAM,IAAI,GACd,MAAM,mBAAmB,CACzB,UAAU,SAAS,MAAM,EAAE,GAAG,QAAQ,KAAK,CAAC,CAC5C,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCF,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EACJ,WAAW,EAAE,GAAG,UAAU;EAC1B,UAAU,EAAE,QAAQ;EACpB,MAAM,EAAE,QAAQ;EAChB,gBAAgB,EAAE,QAAQ;EAC1B,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC;EAChC,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;EACrC,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;EACtC;CACD,SAAS,EAAE,GAAG,mBAAmB;CACjC,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,oBAAoB,KAAY;;CAE9D,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,yBAAyB,SAAS;CAC7C,MAAM,EAAE,oBAAoB,EAAE,GAAG,mBAAmB,EAAE;CACtD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,yBAAyB;AAC9C,QAAM,IAAI,GAAG,OAAO,oBAAoB,mBAAmB;AAC3D,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"codes.js","names":[],"sources":["../../../../src/component/public/identity/codes.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vVerificationCodeDoc } from \"../../model\";\n\n/**\n * Find a verification code by its associated account ID.\n *\n * Queries the `VerificationCode` table using the `account_id` index to locate\n * the unique verification code linked to the given account. Each account has at\n * most one active verification code at a time.\n *\n * @param args.accountId - The document ID of the account whose verification code should be retrieved.\n * @returns The verification code document if one exists for the account, or `null` otherwise.\n *\n * @example\n * ```ts\n * const code = await ctx.runQuery(\n * component.identity.codes.verificationCodeGetByAccountId,\n * { accountId: account._id },\n * );\n * if (code !== null && code.expirationTime > Date.now()) {\n * console.log(\"Active verification code exists\");\n * }\n * ```\n */\nexport const verificationCodeGetByAccountId = query({\n args: { accountId: v.id(\"Account\") },\n returns: v.union(vVerificationCodeDoc, v.null()),\n handler: async (ctx, { accountId }) => {\n return await ctx.db\n .query(\"VerificationCode\")\n .withIndex(\"account_id\", (q) => q.eq(\"accountId\", accountId as any))\n .unique();\n },\n});\n\n/**\n * Find a verification code by its code string value.\n *\n * Queries the `VerificationCode` table using the `code` index to locate the\n * unique verification code document matching the given code string. This is\n * the primary lookup used when a user submits an OTP or clicks a magic link.\n *\n * @param args.code - The verification code string to look up (e.g. a 6-digit OTP or a magic-link token).\n * @returns The verification code document if a match is found, or `null` otherwise.\n *\n * @example\n * ```ts\n * const codeDoc = await ctx.runQuery(\n * component.identity.codes.verificationCodeGetByCode,\n * { code: \"482910\" },\n * );\n * if (codeDoc !== null && codeDoc.expirationTime > Date.now()) {\n * console.log(`Code is valid for account: ${codeDoc.accountId}`);\n * }\n * ```\n */\nexport const verificationCodeGetByCode = query({\n args: { code: v.string() },\n returns: v.union(vVerificationCodeDoc, v.null()),\n handler: async (ctx, { code }) => {\n return await ctx.db\n .query(\"VerificationCode\")\n .withIndex(\"code\", (q) => q.eq(\"code\", code))\n .unique();\n },\n});\n\n/**\n * Create a new verification code for OTP, magic link, or OAuth flows.\n *\n * Inserts a document into the `VerificationCode` table that ties a short-lived\n * code to a specific account and provider. The code can be used for email OTP,\n * phone OTP, magic link, or OAuth state verification depending on the flow.\n *\n * @param args.accountId - The document ID of the account this verification code is associated with.\n * @param args.provider - The name of the authentication provider initiating the verification\n * (e.g. `\"resend-otp\"`, `\"twilio-otp\"`, `\"google\"`).\n * @param args.code - The verification code string (e.g. a random OTP or an opaque token for magic links).\n * @param args.expirationTime - The Unix timestamp (in milliseconds) at which this code expires.\n * @param args.verifier - An optional PKCE verifier string used in OAuth/OIDC flows to prevent CSRF attacks.\n * @param args.emailVerified - An optional email address that will be marked as verified upon successful\n * code redemption.\n * @param args.phoneVerified - An optional phone number that will be marked as verified upon successful\n * code redemption.\n * @returns The document ID of the newly created verification code.\n *\n * @example\n * ```ts\n * const codeId = await ctx.runMutation(\n * component.identity.codes.verificationCodeCreate,\n * {\n * accountId: account._id,\n * provider: \"resend-otp\",\n * code: \"482910\",\n * expirationTime: Date.now() + 10 * 60 * 1000, // 10 minutes\n * emailVerified: \"alice@example.com\",\n * },\n * );\n * ```\n */\nexport const verificationCodeCreate = mutation({\n args: {\n accountId: v.id(\"Account\"),\n provider: v.string(),\n code: v.string(),\n expirationTime: v.number(),\n verifier: v.optional(v.string()),\n emailVerified: v.optional(v.string()),\n phoneVerified: v.optional(v.string()),\n },\n returns: v.id(\"VerificationCode\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"VerificationCode\", args as any);\n },\n});\n\n/**\n * Delete a verification code document permanently.\n *\n * Removes the verification code from the `VerificationCode` table. This is\n * typically called after the code has been successfully redeemed or when it\n * needs to be invalidated (e.g. replaced by a new code).\n *\n * @param args.verificationCodeId - The document ID of the verification code to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // Delete the code after successful verification\n * await ctx.runMutation(\n * component.identity.codes.verificationCodeDelete,\n * { verificationCodeId: codeDoc._id },\n * );\n * ```\n */\nexport const verificationCodeDelete = mutation({\n args: { verificationCodeId: v.id(\"VerificationCode\") },\n returns: v.null(),\n handler: async (ctx, { verificationCodeId }) => {\n await ctx.db.delete(\"VerificationCode\", verificationCodeId);\n return null;\n },\n});\n\n// ============================================================================\n// Refresh Tokens\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,iCAAiC,MAAM;CAClD,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM,sBAAsB,EAAE,MAAM,CAAC;CAChD,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GACd,MAAM,mBAAmB,CACzB,UAAU,eAAe,MAAM,EAAE,GAAG,aAAa,UAAiB,CAAC,CACnE,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBF,MAAa,4BAA4B,MAAM;CAC7C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE;CAC1B,SAAS,EAAE,MAAM,sBAAsB,EAAE,MAAM,CAAC;CAChD,SAAS,OAAO,KAAK,EAAE,WAAW;AAChC,SAAO,MAAM,IAAI,GACd,MAAM,mBAAmB,CACzB,UAAU,SAAS,MAAM,EAAE,GAAG,QAAQ,KAAK,CAAC,CAC5C,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCF,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EACJ,WAAW,EAAE,GAAG,UAAU;EAC1B,UAAU,EAAE,QAAQ;EACpB,MAAM,EAAE,QAAQ;EAChB,gBAAgB,EAAE,QAAQ;EAC1B,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC;EAChC,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;EACrC,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;EACtC;CACD,SAAS,EAAE,GAAG,mBAAmB;CACjC,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,oBAAoB,KAAY;;CAE9D,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,yBAAyB,SAAS;CAC7C,MAAM,EAAE,oBAAoB,EAAE,GAAG,mBAAmB,EAAE;CACtD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,yBAAyB;AAC9C,QAAM,IAAI,GAAG,OAAO,oBAAoB,mBAAmB;AAC3D,SAAO;;CAEV,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","names":[],"sources":["../../../../src/component/public/identity/sessions.ts"],"mappings":";;;;;;;;;;;;;AAkCA;;;;;AAkEA;;;;;AAiCA;;;;;AAgCA;;;;;cAnIa,WAAA;;;;;;;;;;;;;;;;;;;;;;;cAkEA,aAAA;;;;;;;;;;;;;;;;;;;;;;;cAiCA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;cAgCA,aAAA;;;;;;;;;;;;;;;;;;;;cA8BA,iBAAA"}
1
+ {"version":3,"file":"sessions.d.ts","names":[],"sources":["../../../../src/component/public/identity/sessions.ts"],"mappings":";;;;;;;;;;;;;AAmCA;;;;;AAkEA;;;;;AAiCA;;;;;AAgCA;;;;;cAnIa,WAAA;;;;;;;;;;;;;;;;;;;;;;;cAkEA,aAAA;;;;;;;;;;;;;;;;;;;;;;;cAiCA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;;cAgCA,aAAA;;;;;;;;;;;;;;;;;;;;cA8BA,iBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.js","names":[],"sources":["../../../../src/component/public/identity/sessions.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vPaginated, vSessionDoc } from \"../../model\";\n\n/**\n * List sessions with optional filtering and cursor-based pagination.\n *\n * Supports filtering by `userId` to retrieve only sessions belonging to a\n * specific user. When a `userId` filter is provided, the `user_id` index is\n * used for efficient lookup. Results are returned as a paginated response\n * `{ items, nextCursor }` -- pass `nextCursor` back as `cursor` to fetch the\n * next page, or receive `null` when all results have been exhausted.\n *\n * @param args.where - Optional filter object. Currently supports `userId` to\n * restrict results to sessions for a specific user.\n * @param args.limit - Maximum number of sessions to return per page (1--100, default 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor`\n * to continue pagination, or `null` / omitted to start from the beginning.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (default `\"desc\"`).\n * @returns An object with `items` (array of session documents) and `nextCursor`\n * (`string | null`) for fetching subsequent pages.\n *\n * @example\n * ```ts\n * // List the 10 most recent sessions for a user\n * const page = await ctx.runQuery(\n * component.identity.sessions.sessionList,\n * { where: { userId: user._id }, limit: 10, order: \"desc\" },\n * );\n * for (const session of page.items) {\n * console.log(`Session ${session._id} expires at ${session.expirationTime}`);\n * }\n * ```\n */\nexport const sessionList = query({\n args: {\n where: v.optional(\n v.object({\n userId: v.optional(v.id(\"User\")),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vSessionDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n let q;\n if (where.userId !== undefined) {\n q = ctx.db\n .query(\"Session\")\n .withIndex(\"user_id\", (idx) => idx.eq(\"userId\", where.userId!));\n } else {\n q = ctx.db.query(\"Session\");\n }\n\n q = q.order(order);\n\n const all = await q.collect();\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Create a new session for a user with a specified expiration time.\n *\n * Inserts a new document into the `Session` table, linking it to the given user.\n * The session represents an active authenticated context and is typically created\n * after a successful sign-in or token refresh.\n *\n * @param args.userId - The document ID of the user this session belongs to.\n * @param args.expirationTime - The Unix timestamp (in milliseconds) at which this session expires.\n * @returns The document ID of the newly created session.\n *\n * @example\n * ```ts\n * const sessionId = await ctx.runMutation(\n * component.identity.sessions.sessionCreate,\n * {\n * userId: user._id,\n * expirationTime: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days\n * },\n * );\n * ```\n */\nexport const sessionCreate = mutation({\n args: { userId: v.id(\"User\"), expirationTime: v.number() },\n returns: v.id(\"Session\"),\n handler: async (ctx, { userId, expirationTime }) => {\n return await ctx.db.insert(\"Session\", {\n userId: userId as any,\n expirationTime,\n });\n },\n});\n\n/**\n * Retrieve a single session by its Convex document ID.\n *\n * Performs a direct point lookup on the `Session` table. Returns `null` if the\n * session has been deleted or never existed. This does not check whether the\n * session has expired -- callers should compare `expirationTime` to the current\n * time if needed.\n *\n * @param args.sessionId - The Convex document ID (`Id<\"Session\">`) of the session to retrieve.\n * @returns The session document if it exists, or `null` otherwise.\n *\n * @example\n * ```ts\n * const session = await ctx.runQuery(\n * component.identity.sessions.sessionGetById,\n * { sessionId: refreshToken.sessionId },\n * );\n * if (session !== null && session.expirationTime > Date.now()) {\n * console.log(\"Session is still active\");\n * }\n * ```\n */\nexport const sessionGetById = query({\n args: { sessionId: v.id(\"Session\") },\n returns: v.union(vSessionDoc, v.null()),\n handler: async (ctx, { sessionId }) => {\n return await ctx.db.get(\"Session\", sessionId);\n },\n});\n\n/**\n * Delete a session document.\n *\n * Removes the session from the `Session` table. This is a no-op if the session\n * does not exist (i.e. was already deleted). Callers should also clean up\n * related refresh tokens via `refreshTokenDeleteAll` to fully invalidate the\n * session.\n *\n * @param args.sessionId - The document ID of the session to delete.\n * @returns `null` on success (including when the session was already absent).\n *\n * @example\n * ```ts\n * // Revoke a session and its tokens\n * await ctx.runMutation(\n * component.identity.sessions.sessionDelete,\n * { sessionId: session._id },\n * );\n * await ctx.runMutation(\n * component.identity.tokens.refreshTokenDeleteAll,\n * { sessionId: session._id },\n * );\n * ```\n */\nexport const sessionDelete = mutation({\n args: { sessionId: v.id(\"Session\") },\n returns: v.null(),\n handler: async (ctx, { sessionId }) => {\n if ((await ctx.db.get(\"Session\", sessionId)) !== null) {\n await ctx.db.delete(\"Session\", sessionId);\n }\n return null;\n },\n});\n\n/**\n * List all sessions belonging to a specific user.\n *\n * Queries the `Session` table using the `user_id` index to efficiently retrieve\n * every session document for the given user. Unlike `sessionList`, this returns\n * all matching sessions without pagination.\n *\n * @param args.userId - The document ID of the user whose sessions should be retrieved.\n * @returns An array of session documents for the specified user.\n *\n * @example\n * ```ts\n * const sessions = await ctx.runQuery(\n * component.identity.sessions.sessionListByUser,\n * { userId: user._id },\n * );\n * console.log(`User has ${sessions.length} active session(s)`);\n * ```\n */\nexport const sessionListByUser = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vSessionDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"Session\")\n .withIndex(\"user_id\", (q) => q.eq(\"userId\", userId as any))\n .collect();\n },\n});\n\n// ============================================================================\n// Verifiers\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,cAAc,MAAM;CAC/B,MAAM;EACJ,OAAO,EAAE,SACP,EAAE,OAAO,EACP,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,EACjC,CAAC,CACH;EACD,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC7B,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC;EACjD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,MAAM,EAAE,EAAE,QAAQ,OAAO,CAAC,CAAC;EAChE;CACD,SAAS,WAAW,YAAY;CAChC,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,QAAQ,KAAK,SAAS,EAAE;EAC9B,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI,EAAE,EAAE,IAAI;EAC1D,MAAM,QAAQ,KAAK,SAAS;EAE5B,IAAI;AACJ,MAAI,MAAM,WAAW,OACnB,KAAI,IAAI,GACL,MAAM,UAAU,CAChB,UAAU,YAAY,QAAQ,IAAI,GAAG,UAAU,MAAM,OAAQ,CAAC;MAEjE,KAAI,IAAI,GAAG,MAAM,UAAU;AAG7B,MAAI,EAAE,MAAM,MAAM;EAElB,MAAM,MAAM,MAAM,EAAE,SAAS;EAC7B,IAAI,WAAW;AACf,MAAI,KAAK,QAAQ;GACf,MAAM,YAAY,IAAI,WAAW,QAAQ,IAAI,QAAQ,KAAK,OAAO;AACjE,OAAI,cAAc,GAChB,YAAW,YAAY;;EAG3B,MAAM,OAAO,IAAI,MAAM,UAAU,WAAW,QAAQ,EAAE;EACtD,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,UAAU,KAAK,MAAM,GAAG,MAAM,GAAG;AAE/C,SAAO;GAAE;GAAO,YADG,UAAU,MAAM,MAAM,SAAS,GAAG,MAAM;GAC/B;;CAE/B,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,gBAAgB,SAAS;CACpC,MAAM;EAAE,QAAQ,EAAE,GAAG,OAAO;EAAE,gBAAgB,EAAE,QAAQ;EAAE;CAC1D,SAAS,EAAE,GAAG,UAAU;CACxB,SAAS,OAAO,KAAK,EAAE,QAAQ,qBAAqB;AAClD,SAAO,MAAM,IAAI,GAAG,OAAO,WAAW;GAC5B;GACR;GACD,CAAC;;CAEL,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,iBAAiB,MAAM;CAClC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GAAG,IAAI,WAAW,UAAU;;CAEhD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,gBAAgB,SAAS;CACpC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,MAAK,MAAM,IAAI,GAAG,IAAI,WAAW,UAAU,KAAM,KAC/C,OAAM,IAAI,GAAG,OAAO,WAAW,UAAU;AAE3C,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,oBAAoB,MAAM;CACrC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,YAAY;CAC7B,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,YAAY,MAAM,EAAE,GAAG,UAAU,OAAc,CAAC,CAC1D,SAAS;;CAEf,CAAC"}
1
+ {"version":3,"file":"sessions.js","names":[],"sources":["../../../../src/component/public/identity/sessions.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vPaginated, vSessionDoc } from \"../../model\";\n\n/**\n * List sessions with optional filtering and cursor-based pagination.\n *\n * Supports filtering by `userId` to retrieve only sessions belonging to a\n * specific user. When a `userId` filter is provided, the `user_id` index is\n * used for efficient lookup. Results are returned as a paginated response\n * `{ items, nextCursor }` -- pass `nextCursor` back as `cursor` to fetch the\n * next page, or receive `null` when all results have been exhausted.\n *\n * @param args.where - Optional filter object. Currently supports `userId` to\n * restrict results to sessions for a specific user.\n * @param args.limit - Maximum number of sessions to return per page (1--100, default 50).\n * @param args.cursor - An opaque cursor string from a previous response's `nextCursor`\n * to continue pagination, or `null` / omitted to start from the beginning.\n * @param args.order - Sort direction: `\"asc\"` or `\"desc\"` (default `\"desc\"`).\n * @returns An object with `items` (array of session documents) and `nextCursor`\n * (`string | null`) for fetching subsequent pages.\n *\n * @example\n * ```ts\n * // List the 10 most recent sessions for a user\n * const page = await ctx.runQuery(\n * component.identity.sessions.sessionList,\n * { where: { userId: user._id }, limit: 10, order: \"desc\" },\n * );\n * for (const session of page.items) {\n * console.log(`Session ${session._id} expires at ${session.expirationTime}`);\n * }\n * ```\n */\nexport const sessionList = query({\n args: {\n where: v.optional(\n v.object({\n userId: v.optional(v.id(\"User\")),\n }),\n ),\n limit: v.optional(v.number()),\n cursor: v.optional(v.union(v.string(), v.null())),\n order: v.optional(v.union(v.literal(\"asc\"), v.literal(\"desc\"))),\n },\n returns: vPaginated(vSessionDoc),\n handler: async (ctx, args) => {\n const where = args.where ?? {};\n const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);\n const order = args.order ?? \"desc\";\n\n let q;\n if (where.userId !== undefined) {\n q = ctx.db\n .query(\"Session\")\n .withIndex(\"user_id\", (idx) => idx.eq(\"userId\", where.userId!));\n } else {\n q = ctx.db.query(\"Session\");\n }\n\n q = q.order(order);\n\n const all = await q.collect();\n let startIdx = 0;\n if (args.cursor) {\n const cursorIdx = all.findIndex((doc) => doc._id === args.cursor);\n if (cursorIdx !== -1) {\n startIdx = cursorIdx + 1;\n }\n }\n const page = all.slice(startIdx, startIdx + limit + 1);\n const hasMore = page.length > limit;\n const items = hasMore ? page.slice(0, limit) : page;\n const nextCursor = hasMore ? items[items.length - 1]._id : null;\n return { items, nextCursor };\n },\n});\n\n/**\n * Create a new session for a user with a specified expiration time.\n *\n * Inserts a new document into the `Session` table, linking it to the given user.\n * The session represents an active authenticated context and is typically created\n * after a successful sign-in or token refresh.\n *\n * @param args.userId - The document ID of the user this session belongs to.\n * @param args.expirationTime - The Unix timestamp (in milliseconds) at which this session expires.\n * @returns The document ID of the newly created session.\n *\n * @example\n * ```ts\n * const sessionId = await ctx.runMutation(\n * component.identity.sessions.sessionCreate,\n * {\n * userId: user._id,\n * expirationTime: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days\n * },\n * );\n * ```\n */\nexport const sessionCreate = mutation({\n args: { userId: v.id(\"User\"), expirationTime: v.number() },\n returns: v.id(\"Session\"),\n handler: async (ctx, { userId, expirationTime }) => {\n return await ctx.db.insert(\"Session\", {\n userId: userId as any,\n expirationTime,\n });\n },\n});\n\n/**\n * Retrieve a single session by its Convex document ID.\n *\n * Performs a direct point lookup on the `Session` table. Returns `null` if the\n * session has been deleted or never existed. This does not check whether the\n * session has expired -- callers should compare `expirationTime` to the current\n * time if needed.\n *\n * @param args.sessionId - The Convex document ID (`Id<\"Session\">`) of the session to retrieve.\n * @returns The session document if it exists, or `null` otherwise.\n *\n * @example\n * ```ts\n * const session = await ctx.runQuery(\n * component.identity.sessions.sessionGetById,\n * { sessionId: refreshToken.sessionId },\n * );\n * if (session !== null && session.expirationTime > Date.now()) {\n * console.log(\"Session is still active\");\n * }\n * ```\n */\nexport const sessionGetById = query({\n args: { sessionId: v.id(\"Session\") },\n returns: v.union(vSessionDoc, v.null()),\n handler: async (ctx, { sessionId }) => {\n return await ctx.db.get(\"Session\", sessionId);\n },\n});\n\n/**\n * Delete a session document.\n *\n * Removes the session from the `Session` table. This is a no-op if the session\n * does not exist (i.e. was already deleted). Callers should also clean up\n * related refresh tokens via `refreshTokenDeleteAll` to fully invalidate the\n * session.\n *\n * @param args.sessionId - The document ID of the session to delete.\n * @returns `null` on success (including when the session was already absent).\n *\n * @example\n * ```ts\n * // Revoke a session and its tokens\n * await ctx.runMutation(\n * component.identity.sessions.sessionDelete,\n * { sessionId: session._id },\n * );\n * await ctx.runMutation(\n * component.identity.tokens.refreshTokenDeleteAll,\n * { sessionId: session._id },\n * );\n * ```\n */\nexport const sessionDelete = mutation({\n args: { sessionId: v.id(\"Session\") },\n returns: v.null(),\n handler: async (ctx, { sessionId }) => {\n if ((await ctx.db.get(\"Session\", sessionId)) !== null) {\n await ctx.db.delete(\"Session\", sessionId);\n }\n return null;\n },\n});\n\n/**\n * List all sessions belonging to a specific user.\n *\n * Queries the `Session` table using the `user_id` index to efficiently retrieve\n * every session document for the given user. Unlike `sessionList`, this returns\n * all matching sessions without pagination.\n *\n * @param args.userId - The document ID of the user whose sessions should be retrieved.\n * @returns An array of session documents for the specified user.\n *\n * @example\n * ```ts\n * const sessions = await ctx.runQuery(\n * component.identity.sessions.sessionListByUser,\n * { userId: user._id },\n * );\n * console.log(`User has ${sessions.length} active session(s)`);\n * ```\n */\nexport const sessionListByUser = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vSessionDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"Session\")\n .withIndex(\"user_id\", (q) => q.eq(\"userId\", userId as any))\n .collect();\n },\n});\n\n// ============================================================================\n// Verifiers\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,cAAc,MAAM;CAC/B,MAAM;EACJ,OAAO,EAAE,SACP,EAAE,OAAO,EACP,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,EACjC,CAAC,CACH;EACD,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC7B,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC;EACjD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,MAAM,EAAE,EAAE,QAAQ,OAAO,CAAC,CAAC;EAChE;CACD,SAAS,WAAW,YAAY;CAChC,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,QAAQ,KAAK,SAAS,EAAE;EAC9B,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,IAAI,EAAE,EAAE,IAAI;EAC1D,MAAM,QAAQ,KAAK,SAAS;EAE5B,IAAI;AACJ,MAAI,MAAM,WAAW,OACnB,KAAI,IAAI,GACL,MAAM,UAAU,CAChB,UAAU,YAAY,QAAQ,IAAI,GAAG,UAAU,MAAM,OAAQ,CAAC;MAEjE,KAAI,IAAI,GAAG,MAAM,UAAU;AAG7B,MAAI,EAAE,MAAM,MAAM;EAElB,MAAM,MAAM,MAAM,EAAE,SAAS;EAC7B,IAAI,WAAW;AACf,MAAI,KAAK,QAAQ;GACf,MAAM,YAAY,IAAI,WAAW,QAAQ,IAAI,QAAQ,KAAK,OAAO;AACjE,OAAI,cAAc,GAChB,YAAW,YAAY;;EAG3B,MAAM,OAAO,IAAI,MAAM,UAAU,WAAW,QAAQ,EAAE;EACtD,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,UAAU,KAAK,MAAM,GAAG,MAAM,GAAG;AAE/C,SAAO;GAAE;GAAO,YADG,UAAU,MAAM,MAAM,SAAS,GAAG,MAAM;GAC/B;;CAE/B,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,gBAAgB,SAAS;CACpC,MAAM;EAAE,QAAQ,EAAE,GAAG,OAAO;EAAE,gBAAgB,EAAE,QAAQ;EAAE;CAC1D,SAAS,EAAE,GAAG,UAAU;CACxB,SAAS,OAAO,KAAK,EAAE,QAAQ,qBAAqB;AAClD,SAAO,MAAM,IAAI,GAAG,OAAO,WAAW;GAC5B;GACR;GACD,CAAC;;CAEL,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,iBAAiB,MAAM;CAClC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GAAG,IAAI,WAAW,UAAU;;CAEhD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,gBAAgB,SAAS;CACpC,MAAM,EAAE,WAAW,EAAE,GAAG,UAAU,EAAE;CACpC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,MAAK,MAAM,IAAI,GAAG,IAAI,WAAW,UAAU,KAAM,KAC/C,OAAM,IAAI,GAAG,OAAO,WAAW,UAAU;AAE3C,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,oBAAoB,MAAM;CACrC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,YAAY;CAC7B,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,YAAY,MAAM,EAAE,GAAG,UAAU,OAAc,CAAC,CAC1D,SAAS;;CAEf,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","names":[],"sources":["../../../../src/component/public/identity/tokens.ts"],"mappings":";;;;;;;;;;;;;;;AA6BA;;;;;AAgCA;;;;;AA+BA;;;cA/Da,kBAAA;;AAkGb;;;;;AAqCA;;;;;AAiCA;;;;;AAwCA;;;;cAhLa,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;cA+BA,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCA,uBAAA;;;;;;;;;;;;;;;;;;;;cAqCA,yBAAA;;;;;;;;;;;;;;;;;;;;;cAiCA,qBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAwCA,qBAAA"}
1
+ {"version":3,"file":"tokens.d.ts","names":[],"sources":["../../../../src/component/public/identity/tokens.ts"],"mappings":";;;;;;;;;;;;;;;AA8BA;;;;;AAgCA;;;;;AA+BA;;;cA/Da,kBAAA;;AAkGb;;;;;AAqCA;;;;;AAiCA;;;;;AAwCA;;;;cAhLa,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;cA+BA,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCA,uBAAA;;;;;;;;;;;;;;;;;;;;cAqCA,yBAAA;;;;;;;;;;;;;;;;;;;;;cAiCA,qBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAwCA,qBAAA"}