@robelest/convex-auth 0.0.4-preview.21 → 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 (310) 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/convex.config.d.ts +2 -2
  10. package/dist/component/convex.config.d.ts.map +1 -1
  11. package/dist/component/model.d.ts +5 -5
  12. package/dist/component/model.d.ts.map +1 -1
  13. package/dist/component/public/enterprise/audit.d.ts.map +1 -1
  14. package/dist/component/public/enterprise/audit.js.map +1 -1
  15. package/dist/component/public/enterprise/core.d.ts.map +1 -1
  16. package/dist/component/public/enterprise/core.js.map +1 -1
  17. package/dist/component/public/enterprise/domains.d.ts.map +1 -1
  18. package/dist/component/public/enterprise/domains.js.map +1 -1
  19. package/dist/component/public/enterprise/scim.d.ts.map +1 -1
  20. package/dist/component/public/enterprise/scim.js.map +1 -1
  21. package/dist/component/public/enterprise/secrets.d.ts.map +1 -1
  22. package/dist/component/public/enterprise/secrets.js.map +1 -1
  23. package/dist/component/public/enterprise/webhooks.d.ts.map +1 -1
  24. package/dist/component/public/enterprise/webhooks.js.map +1 -1
  25. package/dist/component/public/factors/devices.d.ts.map +1 -1
  26. package/dist/component/public/factors/devices.js.map +1 -1
  27. package/dist/component/public/factors/passkeys.d.ts.map +1 -1
  28. package/dist/component/public/factors/passkeys.js.map +1 -1
  29. package/dist/component/public/factors/totp.d.ts.map +1 -1
  30. package/dist/component/public/factors/totp.js.map +1 -1
  31. package/dist/component/public/groups/core.js.map +1 -1
  32. package/dist/component/public/groups/invites.d.ts.map +1 -1
  33. package/dist/component/public/groups/invites.js.map +1 -1
  34. package/dist/component/public/groups/members.d.ts.map +1 -1
  35. package/dist/component/public/groups/members.js.map +1 -1
  36. package/dist/component/public/identity/accounts.d.ts.map +1 -1
  37. package/dist/component/public/identity/accounts.js.map +1 -1
  38. package/dist/component/public/identity/codes.d.ts.map +1 -1
  39. package/dist/component/public/identity/codes.js.map +1 -1
  40. package/dist/component/public/identity/sessions.d.ts.map +1 -1
  41. package/dist/component/public/identity/sessions.js.map +1 -1
  42. package/dist/component/public/identity/tokens.d.ts.map +1 -1
  43. package/dist/component/public/identity/tokens.js.map +1 -1
  44. package/dist/component/public/identity/users.d.ts.map +1 -1
  45. package/dist/component/public/identity/users.js.map +1 -1
  46. package/dist/component/public/identity/verifiers.d.ts.map +1 -1
  47. package/dist/component/public/identity/verifiers.js.map +1 -1
  48. package/dist/component/public/security/keys.d.ts.map +1 -1
  49. package/dist/component/public/security/keys.js.map +1 -1
  50. package/dist/component/public/security/limits.d.ts.map +1 -1
  51. package/dist/component/public/security/limits.js.map +1 -1
  52. package/dist/component/schema.d.ts +39 -39
  53. package/dist/component/server/auth.d.ts +95 -52
  54. package/dist/component/server/auth.d.ts.map +1 -1
  55. package/dist/component/server/auth.js +63 -43
  56. package/dist/component/server/auth.js.map +1 -1
  57. package/dist/component/server/core.js +116 -235
  58. package/dist/component/server/core.js.map +1 -1
  59. package/dist/component/server/crypto.js +25 -7
  60. package/dist/component/server/crypto.js.map +1 -1
  61. package/dist/component/server/device.js +58 -15
  62. package/dist/component/server/device.js.map +1 -1
  63. package/dist/component/server/enterprise/domain.js +148 -59
  64. package/dist/component/server/enterprise/domain.js.map +1 -1
  65. package/dist/component/server/enterprise/http.js +36 -15
  66. package/dist/component/server/enterprise/http.js.map +1 -1
  67. package/dist/component/server/enterprise/oidc.js +1 -1
  68. package/dist/component/server/http.js +26 -21
  69. package/dist/component/server/http.js.map +1 -1
  70. package/dist/component/server/identity.js +5 -2
  71. package/dist/component/server/identity.js.map +1 -1
  72. package/dist/component/server/limits.js +21 -30
  73. package/dist/component/server/limits.js.map +1 -1
  74. package/dist/component/server/mutations/account.js +12 -10
  75. package/dist/component/server/mutations/account.js.map +1 -1
  76. package/dist/component/server/mutations/code.js +5 -2
  77. package/dist/component/server/mutations/code.js.map +1 -1
  78. package/dist/component/server/mutations/invalidate.js +1 -1
  79. package/dist/component/server/mutations/invalidate.js.map +1 -1
  80. package/dist/component/server/mutations/oauth.js +10 -4
  81. package/dist/component/server/mutations/oauth.js.map +1 -1
  82. package/dist/component/server/mutations/refresh.js +2 -2
  83. package/dist/component/server/mutations/refresh.js.map +1 -1
  84. package/dist/component/server/mutations/register.js +46 -42
  85. package/dist/component/server/mutations/register.js.map +1 -1
  86. package/dist/component/server/mutations/retrieve.js +21 -25
  87. package/dist/component/server/mutations/retrieve.js.map +1 -1
  88. package/dist/component/server/mutations/signature.js +10 -4
  89. package/dist/component/server/mutations/signature.js.map +1 -1
  90. package/dist/component/server/mutations/signout.js.map +1 -1
  91. package/dist/component/server/mutations/store.js +9 -24
  92. package/dist/component/server/mutations/store.js.map +1 -1
  93. package/dist/component/server/mutations/verifier.js.map +1 -1
  94. package/dist/component/server/mutations/verify.js +1 -1
  95. package/dist/component/server/mutations/verify.js.map +1 -1
  96. package/dist/component/server/oauth.js +53 -16
  97. package/dist/component/server/oauth.js.map +1 -1
  98. package/dist/component/server/passkey.js +115 -31
  99. package/dist/component/server/passkey.js.map +1 -1
  100. package/dist/component/server/redirects.js +9 -3
  101. package/dist/component/server/redirects.js.map +1 -1
  102. package/dist/component/server/refresh.js +10 -7
  103. package/dist/component/server/refresh.js.map +1 -1
  104. package/dist/component/server/runtime.d.ts +3 -3
  105. package/dist/component/server/runtime.d.ts.map +1 -1
  106. package/dist/component/server/runtime.js +62 -20
  107. package/dist/component/server/runtime.js.map +1 -1
  108. package/dist/component/server/signin.js +34 -10
  109. package/dist/component/server/signin.js.map +1 -1
  110. package/dist/component/server/totp.js +79 -19
  111. package/dist/component/server/totp.js.map +1 -1
  112. package/dist/component/server/types.d.ts +12 -20
  113. package/dist/component/server/types.d.ts.map +1 -1
  114. package/dist/component/server/types.js.map +1 -1
  115. package/dist/component/server/users.js +6 -3
  116. package/dist/component/server/users.js.map +1 -1
  117. package/dist/component/server/utils.js +10 -4
  118. package/dist/component/server/utils.js.map +1 -1
  119. package/dist/core/types.d.ts +14 -22
  120. package/dist/core/types.d.ts.map +1 -1
  121. package/dist/factors/device.js +8 -9
  122. package/dist/factors/device.js.map +1 -1
  123. package/dist/factors/passkey.js +18 -21
  124. package/dist/factors/passkey.js.map +1 -1
  125. package/dist/providers/password.js +66 -81
  126. package/dist/providers/password.js.map +1 -1
  127. package/dist/runtime/invite.js +2 -8
  128. package/dist/runtime/invite.js.map +1 -1
  129. package/dist/server/auth.d.ts +95 -52
  130. package/dist/server/auth.d.ts.map +1 -1
  131. package/dist/server/auth.js +63 -43
  132. package/dist/server/auth.js.map +1 -1
  133. package/dist/server/core.d.ts +71 -159
  134. package/dist/server/core.d.ts.map +1 -1
  135. package/dist/server/core.js +116 -235
  136. package/dist/server/core.js.map +1 -1
  137. package/dist/server/crypto.d.ts.map +1 -1
  138. package/dist/server/crypto.js +25 -7
  139. package/dist/server/crypto.js.map +1 -1
  140. package/dist/server/device.js +58 -15
  141. package/dist/server/device.js.map +1 -1
  142. package/dist/server/enterprise/domain.d.ts +0 -8
  143. package/dist/server/enterprise/domain.d.ts.map +1 -1
  144. package/dist/server/enterprise/domain.js +148 -59
  145. package/dist/server/enterprise/domain.js.map +1 -1
  146. package/dist/server/enterprise/http.d.ts.map +1 -1
  147. package/dist/server/enterprise/http.js +35 -14
  148. package/dist/server/enterprise/http.js.map +1 -1
  149. package/dist/server/http.d.ts +2 -2
  150. package/dist/server/http.d.ts.map +1 -1
  151. package/dist/server/http.js +25 -20
  152. package/dist/server/http.js.map +1 -1
  153. package/dist/server/identity.js +5 -2
  154. package/dist/server/identity.js.map +1 -1
  155. package/dist/server/index.d.ts +2 -2
  156. package/dist/server/limits.js +21 -30
  157. package/dist/server/limits.js.map +1 -1
  158. package/dist/server/mounts.d.ts +26 -64
  159. package/dist/server/mounts.d.ts.map +1 -1
  160. package/dist/server/mounts.js +45 -106
  161. package/dist/server/mounts.js.map +1 -1
  162. package/dist/server/mutations/account.d.ts +8 -9
  163. package/dist/server/mutations/account.d.ts.map +1 -1
  164. package/dist/server/mutations/account.js +11 -9
  165. package/dist/server/mutations/account.js.map +1 -1
  166. package/dist/server/mutations/code.d.ts +13 -13
  167. package/dist/server/mutations/code.d.ts.map +1 -1
  168. package/dist/server/mutations/code.js +5 -2
  169. package/dist/server/mutations/code.js.map +1 -1
  170. package/dist/server/mutations/invalidate.d.ts +4 -4
  171. package/dist/server/mutations/invalidate.d.ts.map +1 -1
  172. package/dist/server/mutations/invalidate.js.map +1 -1
  173. package/dist/server/mutations/oauth.d.ts +12 -10
  174. package/dist/server/mutations/oauth.d.ts.map +1 -1
  175. package/dist/server/mutations/oauth.js +9 -3
  176. package/dist/server/mutations/oauth.js.map +1 -1
  177. package/dist/server/mutations/refresh.d.ts +3 -3
  178. package/dist/server/mutations/refresh.d.ts.map +1 -1
  179. package/dist/server/mutations/refresh.js +1 -1
  180. package/dist/server/mutations/refresh.js.map +1 -1
  181. package/dist/server/mutations/register.d.ts +11 -11
  182. package/dist/server/mutations/register.d.ts.map +1 -1
  183. package/dist/server/mutations/register.js +45 -41
  184. package/dist/server/mutations/register.js.map +1 -1
  185. package/dist/server/mutations/retrieve.d.ts +6 -6
  186. package/dist/server/mutations/retrieve.d.ts.map +1 -1
  187. package/dist/server/mutations/retrieve.js +20 -24
  188. package/dist/server/mutations/retrieve.js.map +1 -1
  189. package/dist/server/mutations/signature.d.ts +6 -7
  190. package/dist/server/mutations/signature.d.ts.map +1 -1
  191. package/dist/server/mutations/signature.js +9 -3
  192. package/dist/server/mutations/signature.js.map +1 -1
  193. package/dist/server/mutations/signin.d.ts +5 -5
  194. package/dist/server/mutations/signin.d.ts.map +1 -1
  195. package/dist/server/mutations/signout.js.map +1 -1
  196. package/dist/server/mutations/store.d.ts +97 -97
  197. package/dist/server/mutations/store.d.ts.map +1 -1
  198. package/dist/server/mutations/store.js +8 -23
  199. package/dist/server/mutations/store.js.map +1 -1
  200. package/dist/server/mutations/verifier.js.map +1 -1
  201. package/dist/server/mutations/verify.d.ts +10 -10
  202. package/dist/server/mutations/verify.d.ts.map +1 -1
  203. package/dist/server/mutations/verify.js.map +1 -1
  204. package/dist/server/oauth.js +53 -16
  205. package/dist/server/oauth.js.map +1 -1
  206. package/dist/server/passkey.d.ts +2 -2
  207. package/dist/server/passkey.d.ts.map +1 -1
  208. package/dist/server/passkey.js +114 -30
  209. package/dist/server/passkey.js.map +1 -1
  210. package/dist/server/redirects.js +9 -3
  211. package/dist/server/redirects.js.map +1 -1
  212. package/dist/server/refresh.js +10 -7
  213. package/dist/server/refresh.js.map +1 -1
  214. package/dist/server/runtime.d.ts +14 -14
  215. package/dist/server/runtime.d.ts.map +1 -1
  216. package/dist/server/runtime.js +61 -19
  217. package/dist/server/runtime.js.map +1 -1
  218. package/dist/server/signin.js +34 -10
  219. package/dist/server/signin.js.map +1 -1
  220. package/dist/server/ssr.d.ts.map +1 -1
  221. package/dist/server/ssr.js +175 -184
  222. package/dist/server/ssr.js.map +1 -1
  223. package/dist/server/totp.js +78 -18
  224. package/dist/server/totp.js.map +1 -1
  225. package/dist/server/types.d.ts +13 -21
  226. package/dist/server/types.d.ts.map +1 -1
  227. package/dist/server/types.js.map +1 -1
  228. package/dist/server/users.js +6 -3
  229. package/dist/server/users.js.map +1 -1
  230. package/dist/server/utils.js +10 -4
  231. package/dist/server/utils.js.map +1 -1
  232. package/package.json +2 -6
  233. package/src/authorization/index.ts +1 -1
  234. package/src/cli/index.ts +1 -1
  235. package/src/client/core/types.ts +14 -14
  236. package/src/client/factors/device.ts +10 -12
  237. package/src/client/factors/passkey.ts +23 -26
  238. package/src/client/index.ts +54 -64
  239. package/src/client/runtime/invite.ts +5 -7
  240. package/src/component/index.ts +1 -0
  241. package/src/component/public/enterprise/audit.ts +6 -1
  242. package/src/component/public/enterprise/core.ts +1 -0
  243. package/src/component/public/enterprise/domains.ts +5 -1
  244. package/src/component/public/enterprise/scim.ts +1 -0
  245. package/src/component/public/enterprise/secrets.ts +1 -0
  246. package/src/component/public/enterprise/webhooks.ts +1 -0
  247. package/src/component/public/factors/devices.ts +1 -0
  248. package/src/component/public/factors/passkeys.ts +1 -0
  249. package/src/component/public/factors/totp.ts +1 -0
  250. package/src/component/public/groups/core.ts +1 -1
  251. package/src/component/public/groups/invites.ts +7 -1
  252. package/src/component/public/groups/members.ts +1 -0
  253. package/src/component/public/identity/accounts.ts +1 -0
  254. package/src/component/public/identity/codes.ts +1 -0
  255. package/src/component/public/identity/sessions.ts +1 -0
  256. package/src/component/public/identity/tokens.ts +1 -0
  257. package/src/component/public/identity/users.ts +1 -0
  258. package/src/component/public/identity/verifiers.ts +1 -0
  259. package/src/component/public/security/keys.ts +1 -0
  260. package/src/component/public/security/limits.ts +1 -0
  261. package/src/providers/password.ts +89 -110
  262. package/src/server/auth.ts +177 -111
  263. package/src/server/core.ts +197 -233
  264. package/src/server/crypto.ts +31 -29
  265. package/src/server/device.ts +65 -32
  266. package/src/server/enterprise/domain.ts +158 -170
  267. package/src/server/enterprise/http.ts +46 -39
  268. package/src/server/http.ts +36 -30
  269. package/src/server/identity.ts +5 -5
  270. package/src/server/index.ts +2 -0
  271. package/src/server/limits.ts +53 -80
  272. package/src/server/mounts.ts +47 -74
  273. package/src/server/mutations/account.ts +22 -36
  274. package/src/server/mutations/code.ts +6 -6
  275. package/src/server/mutations/invalidate.ts +1 -1
  276. package/src/server/mutations/oauth.ts +14 -8
  277. package/src/server/mutations/refresh.ts +5 -4
  278. package/src/server/mutations/register.ts +87 -132
  279. package/src/server/mutations/retrieve.ts +44 -44
  280. package/src/server/mutations/signature.ts +13 -6
  281. package/src/server/mutations/signout.ts +1 -1
  282. package/src/server/mutations/store.ts +16 -31
  283. package/src/server/mutations/verifier.ts +1 -1
  284. package/src/server/mutations/verify.ts +3 -5
  285. package/src/server/oauth.ts +60 -69
  286. package/src/server/passkey.ts +567 -517
  287. package/src/server/redirects.ts +10 -6
  288. package/src/server/refresh.ts +14 -18
  289. package/src/server/runtime.ts +70 -55
  290. package/src/server/signin.ts +44 -37
  291. package/src/server/ssr.ts +390 -407
  292. package/src/server/totp.ts +85 -35
  293. package/src/server/types.ts +19 -22
  294. package/src/server/users.ts +7 -6
  295. package/src/server/utils.ts +10 -12
  296. package/dist/component/server/authError.js +0 -34
  297. package/dist/component/server/authError.js.map +0 -1
  298. package/dist/component/server/errors.d.ts +0 -1
  299. package/dist/component/server/errors.js +0 -137
  300. package/dist/component/server/errors.js.map +0 -1
  301. package/dist/server/authError.d.ts +0 -46
  302. package/dist/server/authError.d.ts.map +0 -1
  303. package/dist/server/authError.js +0 -34
  304. package/dist/server/authError.js.map +0 -1
  305. package/dist/server/errors.d.ts +0 -177
  306. package/dist/server/errors.d.ts.map +0 -1
  307. package/dist/server/errors.js +0 -212
  308. package/dist/server/errors.js.map +0 -1
  309. package/src/server/authError.ts +0 -44
  310. package/src/server/errors.ts +0 -290
@@ -1 +1 @@
1
- {"version":3,"file":"domains.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/domains.ts"],"mappings":";;;;;;;;;;;;;;;AAgCA;;;;;AA2EA;;;;;AA4BA;;;;;AAoCA;cA3Ia,mBAAA;;;;AA2Lb;;;;;AA4CA;;;;;AAmCA;;;;;;;;cA/La,oBAAA;;;;;;;;;;;;;;;;;;cA4BA,sBAAA;;;;;;;;;;;;;;;;;;;;;cAoCA,+BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgDA,kCAAA;;;;;;;;;;;;;;;;;;;cA4CA,kCAAA;;;;;;;;;;;;;;;;;;;;;cAmCA,sBAAA"}
1
+ {"version":3,"file":"domains.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/domains.ts"],"mappings":";;;;;;;;;;;;;;;AAoCA;;;;;AA2EA;;;;;AA4BA;;;;;AAoCA;cA3Ia,mBAAA;;;;AA2Lb;;;;;AA4CA;;;;;AAmCA;;;;;;;;cA/La,oBAAA;;;;;;;;;;;;;;;;;;cA4BA,sBAAA;;;;;;;;;;;;;;;;;;;;;cAoCA,+BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgDA,kCAAA;;;;;;;;;;;;;;;;;;;cA4CA,kCAAA;;;;;;;;;;;;;;;;;;;;;cAmCA,sBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"domains.js","names":[],"sources":["../../../../src/component/public/enterprise/domains.ts"],"sourcesContent":["import { ConvexError, v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vEnterpriseDomainDoc, vEnterpriseDomainVerificationDoc } from \"../../model\";\n\n/**\n * Link a domain to an enterprise record, or update an existing link.\n *\n * If the domain is already attached to a different enterprise, an\n * `ENTERPRISE_DOMAIN_TAKEN` error is thrown. If the domain already exists for\n * this enterprise, it is updated in place (e.g. toggling `isPrimary`). When\n * `isPrimary` is `true`, any previously primary domain on the same enterprise\n * is demoted. The first domain added to an enterprise becomes primary by default.\n *\n * @param args.enterpriseId - The ID of the enterprise to attach the domain to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.domain - The domain name to link (e.g. `\"acme.com\"`).\n * @param args.isPrimary - Whether this domain should be set as the primary domain for the enterprise. Defaults to `true` for the first domain.\n * @returns The ID of the created or updated `EnterpriseDomain` document.\n *\n * @example\n * ```ts\n * const domainId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainAdd,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * domain: \"acme.com\",\n * isPrimary: true,\n * },\n * );\n * ```\n */\nexport const enterpriseDomainAdd = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n domain: v.string(),\n isPrimary: v.optional(v.boolean()),\n },\n returns: v.id(\"EnterpriseDomain\"),\n handler: async (ctx, args) => {\n const existingByDomain = await ctx.db\n .query(\"EnterpriseDomain\")\n .withIndex(\"domain\", (idx) => idx.eq(\"domain\", args.domain))\n .first();\n if (\n existingByDomain &&\n existingByDomain.enterpriseId !== args.enterpriseId\n ) {\n throw new ConvexError({\n code: \"ENTERPRISE_DOMAIN_TAKEN\",\n message: \"That domain is already attached to another enterprise.\",\n });\n }\n\n const existingForEnterprise = await ctx.db\n .query(\"EnterpriseDomain\")\n .withIndex(\"enterprise_id\", (idx) =>\n idx.eq(\"enterpriseId\", args.enterpriseId),\n )\n .collect();\n\n for (const row of existingForEnterprise) {\n if (row.domain === args.domain) {\n await ctx.db.patch(row._id, {\n isPrimary: args.isPrimary ?? row.isPrimary,\n });\n return row._id;\n }\n }\n\n if (args.isPrimary === true) {\n for (const row of existingForEnterprise) {\n if (row.isPrimary) {\n await ctx.db.patch(row._id, { isPrimary: false });\n }\n }\n }\n\n return await ctx.db.insert(\"EnterpriseDomain\", {\n ...args,\n isPrimary: args.isPrimary ?? existingForEnterprise.length === 0,\n });\n },\n});\n\n/**\n * List all domains linked to a specific enterprise.\n *\n * Returns all `EnterpriseDomain` documents associated with the given enterprise,\n * queried via the `enterprise_id` index. The result includes both verified and\n * unverified domains.\n *\n * @param args.enterpriseId - The ID of the enterprise whose domains to list.\n * @returns An array of enterprise domain documents.\n *\n * @example\n * ```ts\n * const domains = await ctx.runQuery(\n * components.auth.enterprise.enterpriseDomainList,\n * { enterpriseId },\n * );\n * for (const d of domains) {\n * console.log(d.domain, d.isPrimary, d.verifiedAt);\n * }\n * ```\n */\nexport const enterpriseDomainList = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.array(vEnterpriseDomainDoc),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseDomain\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .collect();\n },\n});\n\n/**\n * Remove a linked enterprise domain and its associated verification record.\n *\n * Deletes the `EnterpriseDomain` document and, if one exists, the related\n * `EnterpriseDomainVerification` record. This is a permanent deletion.\n *\n * @param args.domainId - The document ID of the enterprise domain to remove.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainDelete,\n * { domainId },\n * );\n * ```\n */\nexport const enterpriseDomainDelete = mutation({\n args: { domainId: v.id(\"EnterpriseDomain\") },\n returns: v.null(),\n handler: async (ctx, { domainId }) => {\n const verification = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n if (verification) {\n await ctx.db.delete(verification._id);\n }\n await ctx.db.delete(domainId);\n return null;\n },\n});\n\n/**\n * Retrieve the pending domain verification record for a given enterprise domain.\n *\n * Returns the `EnterpriseDomainVerification` document associated with the\n * specified domain, or `null` if no verification has been initiated.\n *\n * @param args.domainId - The document ID of the enterprise domain whose verification to retrieve.\n * @returns The domain verification document, or `null` if none exists.\n *\n * @example\n * ```ts\n * const verification = await ctx.runQuery(\n * components.auth.enterprise.enterpriseDomainVerificationGet,\n * { domainId },\n * );\n * if (verification) {\n * console.log(verification.recordName, verification.expiresAt);\n * }\n * ```\n */\nexport const enterpriseDomainVerificationGet = query({\n args: { domainId: v.id(\"EnterpriseDomain\") },\n returns: v.union(vEnterpriseDomainVerificationDoc, v.null()),\n handler: async (ctx, { domainId }) => {\n return await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n },\n});\n\n/**\n * Create or update a domain verification challenge for an enterprise domain.\n *\n * If a verification record already exists for the domain, all fields are\n * updated in place (e.g. to rotate the token). Otherwise a new record is\n * created. The caller is responsible for generating the DNS record name,\n * token, and token hash.\n *\n * @param args.enterpriseId - The ID of the enterprise that owns the domain.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.domainId - The document ID of the enterprise domain to verify.\n * @param args.domain - The domain name string (e.g. `\"acme.com\"`).\n * @param args.recordName - The DNS TXT record name to be published (e.g. `\"_convex-verify.acme.com\"`).\n * @param args.token - The plaintext verification token value.\n * @param args.tokenHash - A hash of the verification token for secure storage.\n * @param args.requestedAt - Epoch timestamp (ms) when the verification was requested.\n * @param args.expiresAt - Epoch timestamp (ms) after which the challenge expires.\n * @returns The ID of the created or updated `EnterpriseDomainVerification` document.\n *\n * @example\n * ```ts\n * const verificationId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainVerificationUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * domainId,\n * domain: \"acme.com\",\n * recordName: \"_convex-verify.acme.com\",\n * token: \"abc123\",\n * tokenHash: \"sha256:...\",\n * requestedAt: Date.now(),\n * expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,\n * },\n * );\n * ```\n */\nexport const enterpriseDomainVerificationUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n domainId: v.id(\"EnterpriseDomain\"),\n domain: v.string(),\n recordName: v.string(),\n token: v.string(),\n tokenHash: v.string(),\n requestedAt: v.number(),\n expiresAt: v.number(),\n },\n returns: v.id(\"EnterpriseDomainVerification\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", args.domainId))\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseDomainVerification\", args);\n },\n});\n\n/**\n * Delete the pending domain verification record for an enterprise domain.\n *\n * Removes the `EnterpriseDomainVerification` document associated with the\n * given domain, effectively cancelling the verification challenge. If no\n * verification record exists, this is a no-op.\n *\n * @param args.domainId - The document ID of the enterprise domain whose verification to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainVerificationDelete,\n * { domainId },\n * );\n * ```\n */\nexport const enterpriseDomainVerificationDelete = mutation({\n args: { domainId: v.id(\"EnterpriseDomain\") },\n returns: v.null(),\n handler: async (ctx, { domainId }) => {\n const existing = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n if (existing) {\n await ctx.db.delete(existing._id);\n }\n return null;\n },\n});\n\n/**\n * Mark an enterprise domain as verified and clean up the verification record.\n *\n * Sets the `verifiedAt` timestamp on the domain document and deletes the\n * associated `EnterpriseDomainVerification` record (if any). Throws an\n * `INVALID_PARAMETERS` error if the domain document does not exist.\n *\n * @param args.domainId - The document ID of the enterprise domain to mark as verified.\n * @param args.verifiedAt - Epoch timestamp (ms) at which the domain was verified.\n * @returns The updated enterprise domain document with the `verifiedAt` field set.\n *\n * @example\n * ```ts\n * const verifiedDomain = await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainVerify,\n * { domainId, verifiedAt: Date.now() },\n * );\n * console.log(\"Domain verified:\", verifiedDomain.domain);\n * ```\n */\nexport const enterpriseDomainVerify = mutation({\n args: {\n domainId: v.id(\"EnterpriseDomain\"),\n verifiedAt: v.number(),\n },\n returns: vEnterpriseDomainDoc,\n handler: async (ctx, { domainId, verifiedAt }) => {\n await ctx.db.patch(domainId, { verifiedAt });\n const domain = await ctx.db.get(\"EnterpriseDomain\", domainId);\n if (!domain) {\n throw new ConvexError({\n code: \"INVALID_PARAMETERS\",\n message: \"Enterprise domain not found.\",\n });\n }\n const verification = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n if (verification) {\n await ctx.db.delete(verification._id);\n }\n return domain;\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"domains.js","names":[],"sources":["../../../../src/component/public/enterprise/domains.ts"],"sourcesContent":["import { ConvexError, v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport {\n vEnterpriseDomainDoc,\n vEnterpriseDomainVerificationDoc,\n} from \"../../model\";\n\n/**\n * Link a domain to an enterprise record, or update an existing link.\n *\n * If the domain is already attached to a different enterprise, an\n * `ENTERPRISE_DOMAIN_TAKEN` error is thrown. If the domain already exists for\n * this enterprise, it is updated in place (e.g. toggling `isPrimary`). When\n * `isPrimary` is `true`, any previously primary domain on the same enterprise\n * is demoted. The first domain added to an enterprise becomes primary by default.\n *\n * @param args.enterpriseId - The ID of the enterprise to attach the domain to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.domain - The domain name to link (e.g. `\"acme.com\"`).\n * @param args.isPrimary - Whether this domain should be set as the primary domain for the enterprise. Defaults to `true` for the first domain.\n * @returns The ID of the created or updated `EnterpriseDomain` document.\n *\n * @example\n * ```ts\n * const domainId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainAdd,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * domain: \"acme.com\",\n * isPrimary: true,\n * },\n * );\n * ```\n */\nexport const enterpriseDomainAdd = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n domain: v.string(),\n isPrimary: v.optional(v.boolean()),\n },\n returns: v.id(\"EnterpriseDomain\"),\n handler: async (ctx, args) => {\n const existingByDomain = await ctx.db\n .query(\"EnterpriseDomain\")\n .withIndex(\"domain\", (idx) => idx.eq(\"domain\", args.domain))\n .first();\n if (\n existingByDomain &&\n existingByDomain.enterpriseId !== args.enterpriseId\n ) {\n throw new ConvexError({\n code: \"ENTERPRISE_DOMAIN_TAKEN\",\n message: \"That domain is already attached to another enterprise.\",\n });\n }\n\n const existingForEnterprise = await ctx.db\n .query(\"EnterpriseDomain\")\n .withIndex(\"enterprise_id\", (idx) =>\n idx.eq(\"enterpriseId\", args.enterpriseId),\n )\n .collect();\n\n for (const row of existingForEnterprise) {\n if (row.domain === args.domain) {\n await ctx.db.patch(row._id, {\n isPrimary: args.isPrimary ?? row.isPrimary,\n });\n return row._id;\n }\n }\n\n if (args.isPrimary === true) {\n for (const row of existingForEnterprise) {\n if (row.isPrimary) {\n await ctx.db.patch(row._id, { isPrimary: false });\n }\n }\n }\n\n return await ctx.db.insert(\"EnterpriseDomain\", {\n ...args,\n isPrimary: args.isPrimary ?? existingForEnterprise.length === 0,\n });\n },\n});\n\n/**\n * List all domains linked to a specific enterprise.\n *\n * Returns all `EnterpriseDomain` documents associated with the given enterprise,\n * queried via the `enterprise_id` index. The result includes both verified and\n * unverified domains.\n *\n * @param args.enterpriseId - The ID of the enterprise whose domains to list.\n * @returns An array of enterprise domain documents.\n *\n * @example\n * ```ts\n * const domains = await ctx.runQuery(\n * components.auth.enterprise.enterpriseDomainList,\n * { enterpriseId },\n * );\n * for (const d of domains) {\n * console.log(d.domain, d.isPrimary, d.verifiedAt);\n * }\n * ```\n */\nexport const enterpriseDomainList = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.array(vEnterpriseDomainDoc),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseDomain\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .collect();\n },\n});\n\n/**\n * Remove a linked enterprise domain and its associated verification record.\n *\n * Deletes the `EnterpriseDomain` document and, if one exists, the related\n * `EnterpriseDomainVerification` record. This is a permanent deletion.\n *\n * @param args.domainId - The document ID of the enterprise domain to remove.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainDelete,\n * { domainId },\n * );\n * ```\n */\nexport const enterpriseDomainDelete = mutation({\n args: { domainId: v.id(\"EnterpriseDomain\") },\n returns: v.null(),\n handler: async (ctx, { domainId }) => {\n const verification = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n if (verification) {\n await ctx.db.delete(verification._id);\n }\n await ctx.db.delete(domainId);\n return null;\n },\n});\n\n/**\n * Retrieve the pending domain verification record for a given enterprise domain.\n *\n * Returns the `EnterpriseDomainVerification` document associated with the\n * specified domain, or `null` if no verification has been initiated.\n *\n * @param args.domainId - The document ID of the enterprise domain whose verification to retrieve.\n * @returns The domain verification document, or `null` if none exists.\n *\n * @example\n * ```ts\n * const verification = await ctx.runQuery(\n * components.auth.enterprise.enterpriseDomainVerificationGet,\n * { domainId },\n * );\n * if (verification) {\n * console.log(verification.recordName, verification.expiresAt);\n * }\n * ```\n */\nexport const enterpriseDomainVerificationGet = query({\n args: { domainId: v.id(\"EnterpriseDomain\") },\n returns: v.union(vEnterpriseDomainVerificationDoc, v.null()),\n handler: async (ctx, { domainId }) => {\n return await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n },\n});\n\n/**\n * Create or update a domain verification challenge for an enterprise domain.\n *\n * If a verification record already exists for the domain, all fields are\n * updated in place (e.g. to rotate the token). Otherwise a new record is\n * created. The caller is responsible for generating the DNS record name,\n * token, and token hash.\n *\n * @param args.enterpriseId - The ID of the enterprise that owns the domain.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.domainId - The document ID of the enterprise domain to verify.\n * @param args.domain - The domain name string (e.g. `\"acme.com\"`).\n * @param args.recordName - The DNS TXT record name to be published (e.g. `\"_convex-verify.acme.com\"`).\n * @param args.token - The plaintext verification token value.\n * @param args.tokenHash - A hash of the verification token for secure storage.\n * @param args.requestedAt - Epoch timestamp (ms) when the verification was requested.\n * @param args.expiresAt - Epoch timestamp (ms) after which the challenge expires.\n * @returns The ID of the created or updated `EnterpriseDomainVerification` document.\n *\n * @example\n * ```ts\n * const verificationId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainVerificationUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * domainId,\n * domain: \"acme.com\",\n * recordName: \"_convex-verify.acme.com\",\n * token: \"abc123\",\n * tokenHash: \"sha256:...\",\n * requestedAt: Date.now(),\n * expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,\n * },\n * );\n * ```\n */\nexport const enterpriseDomainVerificationUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n domainId: v.id(\"EnterpriseDomain\"),\n domain: v.string(),\n recordName: v.string(),\n token: v.string(),\n tokenHash: v.string(),\n requestedAt: v.number(),\n expiresAt: v.number(),\n },\n returns: v.id(\"EnterpriseDomainVerification\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", args.domainId))\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseDomainVerification\", args);\n },\n});\n\n/**\n * Delete the pending domain verification record for an enterprise domain.\n *\n * Removes the `EnterpriseDomainVerification` document associated with the\n * given domain, effectively cancelling the verification challenge. If no\n * verification record exists, this is a no-op.\n *\n * @param args.domainId - The document ID of the enterprise domain whose verification to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainVerificationDelete,\n * { domainId },\n * );\n * ```\n */\nexport const enterpriseDomainVerificationDelete = mutation({\n args: { domainId: v.id(\"EnterpriseDomain\") },\n returns: v.null(),\n handler: async (ctx, { domainId }) => {\n const existing = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n if (existing) {\n await ctx.db.delete(existing._id);\n }\n return null;\n },\n});\n\n/**\n * Mark an enterprise domain as verified and clean up the verification record.\n *\n * Sets the `verifiedAt` timestamp on the domain document and deletes the\n * associated `EnterpriseDomainVerification` record (if any). Throws an\n * `INVALID_PARAMETERS` error if the domain document does not exist.\n *\n * @param args.domainId - The document ID of the enterprise domain to mark as verified.\n * @param args.verifiedAt - Epoch timestamp (ms) at which the domain was verified.\n * @returns The updated enterprise domain document with the `verifiedAt` field set.\n *\n * @example\n * ```ts\n * const verifiedDomain = await ctx.runMutation(\n * components.auth.enterprise.enterpriseDomainVerify,\n * { domainId, verifiedAt: Date.now() },\n * );\n * console.log(\"Domain verified:\", verifiedDomain.domain);\n * ```\n */\nexport const enterpriseDomainVerify = mutation({\n args: {\n domainId: v.id(\"EnterpriseDomain\"),\n verifiedAt: v.number(),\n },\n returns: vEnterpriseDomainDoc,\n handler: async (ctx, { domainId, verifiedAt }) => {\n await ctx.db.patch(domainId, { verifiedAt });\n const domain = await ctx.db.get(\"EnterpriseDomain\", domainId);\n if (!domain) {\n throw new ConvexError({\n code: \"INVALID_PARAMETERS\",\n message: \"Enterprise domain not found.\",\n });\n }\n const verification = await ctx.db\n .query(\"EnterpriseDomainVerification\")\n .withIndex(\"domain_id\", (idx) => idx.eq(\"domainId\", domainId))\n .first();\n if (verification) {\n await ctx.db.delete(verification._id);\n }\n return domain;\n },\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"scim.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/scim.ts"],"mappings":";;;;;;;;;;;;;;;;;;AAwCA;;;;;AA8CA;;;;;AA+BA;;;;;AAmCA;cAhHa,0BAAA;;;;AAyJb;;;;;AA8BA;;;;;AAqCA;;;;;AAiCA;;cA/Ma,mCAAA;;;AA8Pb;;;;;AAgDA;;;;;;;;;;;;;cA/Qa,kCAAA;;;;;;;;;;;;;;;;;;;;;;;;;cAmCA,yBAAA;;;;;;;;;;;;;;;;;;;;;;cAyCA,+BAAA;;;;;;;;;;;;;;;;;;;;cA8BA,4CAAA;;;;;;;;;;;;;;;;;;;;;;cAqCA,sCAAA;;;;;;;;;;;;;;;;;;;;;cAiCA,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+CA,4BAAA;;;;;;;;;;;;;;;;;;cAgDA,4BAAA"}
1
+ {"version":3,"file":"scim.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/scim.ts"],"mappings":";;;;;;;;;;;;;;;;;;AAyCA;;;;;AA8CA;;;;;AA+BA;;;;;AAmCA;cAhHa,0BAAA;;;;AAyJb;;;;;AA8BA;;;;;AAqCA;;;;;AAiCA;;cA/Ma,mCAAA;;;AA8Pb;;;;;AAgDA;;;;;;;;;;;;;cA/Qa,kCAAA;;;;;;;;;;;;;;;;;;;;;;;;;cAmCA,yBAAA;;;;;;;;;;;;;;;;;;;;;;cAyCA,+BAAA;;;;;;;;;;;;;;;;;;;;cA8BA,4CAAA;;;;;;;;;;;;;;;;;;;;;;cAqCA,sCAAA;;;;;;;;;;;;;;;;;;;;;cAiCA,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+CA,4BAAA;;;;;;;;;;;;;;;;;;cAgDA,4BAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"scim.js","names":[],"sources":["../../../../src/component/public/enterprise/scim.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport {\n vEnterpriseScimConfigDoc,\n vEnterpriseScimIdentityDoc,\n vScimResourceType,\n vScimStatus,\n} from \"../../model\";\n\n/**\n * Create or update the SCIM provisioning configuration for an enterprise.\n *\n * If a SCIM config already exists for the given enterprise, all fields are\n * patched in place (useful for rotating the bearer token). Otherwise a new\n * config document is created. Only one SCIM config is allowed per enterprise.\n *\n * @param args.enterpriseId - The ID of the enterprise to configure SCIM for.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.status - The SCIM config lifecycle status: `\"draft\"`, `\"active\"`, or `\"disabled\"`.\n * @param args.basePath - The base URL path for the SCIM endpoint (e.g. `\"/scim/v2\"`).\n * @param args.tokenHash - A hash of the bearer token used to authenticate SCIM requests.\n * @param args.lastRotatedAt - An optional epoch timestamp (ms) recording when the token was last rotated.\n * @param args.extend - An optional arbitrary extension object for custom SCIM settings.\n * @returns The ID of the created or updated `EnterpriseScimConfig` document.\n *\n * @example\n * ```ts\n * const configId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseScimConfigUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * status: \"active\",\n * basePath: \"/scim/v2\",\n * tokenHash: \"sha256:abc123...\",\n * lastRotatedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const enterpriseScimConfigUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n status: vScimStatus,\n basePath: v.string(),\n tokenHash: v.string(),\n lastRotatedAt: v.optional(v.number()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"EnterpriseScimConfig\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseScimConfig\")\n .withIndex(\"enterprise_id\", (idx) =>\n idx.eq(\"enterpriseId\", args.enterpriseId),\n )\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseScimConfig\", args);\n },\n});\n\n/**\n * Retrieve the SCIM configuration for a specific enterprise.\n *\n * Looks up the SCIM config document by enterprise ID using the\n * `enterprise_id` index. Returns `null` if SCIM has not been configured.\n *\n * @param args.enterpriseId - The ID of the enterprise whose SCIM config to retrieve.\n * @returns The SCIM configuration document, or `null` if not configured.\n *\n * @example\n * ```ts\n * const config = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimConfigGetByEnterprise,\n * { enterpriseId },\n * );\n * if (config) {\n * console.log(config.status, config.basePath);\n * }\n * ```\n */\nexport const enterpriseScimConfigGetByEnterprise = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.union(vEnterpriseScimConfigDoc, v.null()),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseScimConfig\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .first();\n },\n});\n\n/**\n * Look up a SCIM configuration by its bearer token hash.\n *\n * Used during SCIM request authentication to resolve which enterprise a\n * given bearer token belongs to. Returns `null` if no config matches.\n *\n * @param args.tokenHash - The hash of the bearer token from the incoming SCIM request.\n * @returns The matching SCIM configuration document, or `null` if not found.\n *\n * @example\n * ```ts\n * const config = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimConfigGetByTokenHash,\n * { tokenHash: \"sha256:abc123...\" },\n * );\n * if (config) {\n * console.log(\"Authenticated enterprise:\", config.enterpriseId);\n * }\n * ```\n */\nexport const enterpriseScimConfigGetByTokenHash = query({\n args: { tokenHash: v.string() },\n returns: v.union(vEnterpriseScimConfigDoc, v.null()),\n handler: async (ctx, { tokenHash }) => {\n return await ctx.db\n .query(\"EnterpriseScimConfig\")\n .withIndex(\"token_hash\", (idx) => idx.eq(\"tokenHash\", tokenHash))\n .first();\n },\n});\n\n/**\n * Retrieve a SCIM identity by enterprise, resource type, and external ID.\n *\n * Looks up a SCIM-provisioned identity using the composite index on\n * `(enterpriseId, resourceType, externalId)`. This is the primary lookup\n * used when processing incoming SCIM user or group operations.\n *\n * @param args.enterpriseId - The ID of the enterprise that owns the SCIM identity.\n * @param args.resourceType - The SCIM resource type: `\"user\"` or `\"group\"`.\n * @param args.externalId - The external identifier assigned by the identity provider.\n * @returns The SCIM identity document, or `null` if not found.\n *\n * @example\n * ```ts\n * const identity = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGet,\n * {\n * enterpriseId,\n * resourceType: \"user\",\n * externalId: \"okta-user-abc123\",\n * },\n * );\n * ```\n */\nexport const enterpriseScimIdentityGet = query({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n resourceType: vScimResourceType,\n externalId: v.string(),\n },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, args) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id_resource_type_external_id\", (idx) =>\n idx\n .eq(\"enterpriseId\", args.enterpriseId)\n .eq(\"resourceType\", args.resourceType)\n .eq(\"externalId\", args.externalId),\n )\n .first();\n },\n});\n\n/**\n * Retrieve the SCIM identity linked to a specific user.\n *\n * Looks up the first SCIM identity document associated with the given user ID\n * via the `user_id` index. Useful for checking whether a user was provisioned\n * through SCIM.\n *\n * @param args.userId - The document ID of the user whose SCIM identity to retrieve.\n * @returns The SCIM identity document, or `null` if the user has no SCIM identity.\n *\n * @example\n * ```ts\n * const scimIdentity = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGetByUser,\n * { userId },\n * );\n * if (scimIdentity) {\n * console.log(\"User provisioned via SCIM:\", scimIdentity.externalId);\n * }\n * ```\n */\nexport const enterpriseScimIdentityGetByUser = query({\n args: { userId: v.id(\"User\") },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"user_id\", (idx) => idx.eq(\"userId\", userId))\n .first();\n },\n});\n\n/**\n * Retrieve the SCIM identity for a specific user within a specific enterprise.\n *\n * Uses the composite `(enterpriseId, userId)` index to find the SCIM identity\n * that links a user to a particular enterprise. This is useful when a user may\n * belong to multiple enterprises.\n *\n * @param args.enterpriseId - The ID of the enterprise to scope the lookup to.\n * @param args.userId - The document ID of the user.\n * @returns The SCIM identity document, or `null` if not found.\n *\n * @example\n * ```ts\n * const identity = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGetByEnterpriseAndUser,\n * { enterpriseId, userId },\n * );\n * ```\n */\nexport const enterpriseScimIdentityGetByEnterpriseAndUser = query({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n userId: v.id(\"User\"),\n },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, { enterpriseId, userId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id_user_id\", (idx) =>\n idx.eq(\"enterpriseId\", enterpriseId).eq(\"userId\", userId),\n )\n .first();\n },\n});\n\n/**\n * Retrieve the SCIM identity that is mapped to a specific group.\n *\n * Looks up a SCIM identity by its `mappedGroupId` field. This is used when\n * a SCIM group resource has been mapped to an internal group, and you need\n * to find the corresponding SCIM identity record.\n *\n * @param args.mappedGroupId - The document ID of the internal group that a SCIM group is mapped to.\n * @returns The SCIM identity document, or `null` if no mapping exists.\n *\n * @example\n * ```ts\n * const scimGroup = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGetByMappedGroup,\n * { mappedGroupId: teamGroupId },\n * );\n * if (scimGroup) {\n * console.log(\"SCIM external group ID:\", scimGroup.externalId);\n * }\n * ```\n */\nexport const enterpriseScimIdentityGetByMappedGroup = query({\n args: { mappedGroupId: v.id(\"Group\") },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, { mappedGroupId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"mapped_group_id\", (idx) =>\n idx.eq(\"mappedGroupId\", mappedGroupId),\n )\n .first();\n },\n});\n\n/**\n * List all SCIM identities belonging to a specific enterprise.\n *\n * Returns all `EnterpriseScimIdentity` documents for the given enterprise,\n * including both user and group resource types. Useful for displaying all\n * SCIM-provisioned resources or for bulk operations.\n *\n * @param args.enterpriseId - The ID of the enterprise whose SCIM identities to list.\n * @returns An array of SCIM identity documents.\n *\n * @example\n * ```ts\n * const identities = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityListByEnterprise,\n * { enterpriseId },\n * );\n * const users = identities.filter((i) => i.resourceType === \"user\");\n * const groups = identities.filter((i) => i.resourceType === \"group\");\n * ```\n */\nexport const enterpriseScimIdentityListByEnterprise = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.array(vEnterpriseScimIdentityDoc),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .collect();\n },\n});\n\n/**\n * Create or update a SCIM-provisioned identity record.\n *\n * If a SCIM identity with the same `(enterpriseId, resourceType, externalId)`\n * already exists, its fields are patched in place. Otherwise a new record is\n * created. This is the core upsert used by the SCIM provisioning handler to\n * sync users and groups from external identity providers.\n *\n * @param args.enterpriseId - The ID of the enterprise the identity belongs to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.resourceType - The SCIM resource type: `\"user\"` or `\"group\"`.\n * @param args.externalId - The external identifier assigned by the identity provider.\n * @param args.userId - An optional link to the internal user document (for user resources).\n * @param args.mappedGroupId - An optional link to an internal group document (for group resources).\n * @param args.lastProvisionedAt - An optional epoch timestamp (ms) of the last sync.\n * @param args.active - An optional flag indicating whether the identity is active.\n * @param args.raw - An optional raw SCIM payload stored for debugging or re-processing.\n * @returns The ID of the created or updated `EnterpriseScimIdentity` document.\n *\n * @example\n * ```ts\n * const identityId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseScimIdentityUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * resourceType: \"user\",\n * externalId: \"okta-user-abc123\",\n * userId,\n * active: true,\n * lastProvisionedAt: Date.now(),\n * raw: { schemas: [\"urn:ietf:params:scim:schemas:core:2.0:User\"], userName: \"jane@acme.com\" },\n * },\n * );\n * ```\n */\nexport const enterpriseScimIdentityUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n resourceType: vScimResourceType,\n externalId: v.string(),\n userId: v.optional(v.id(\"User\")),\n mappedGroupId: v.optional(v.id(\"Group\")),\n lastProvisionedAt: v.optional(v.number()),\n active: v.optional(v.boolean()),\n raw: v.optional(v.any()),\n },\n returns: v.id(\"EnterpriseScimIdentity\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id_resource_type_external_id\", (idx) =>\n idx\n .eq(\"enterpriseId\", args.enterpriseId)\n .eq(\"resourceType\", args.resourceType)\n .eq(\"externalId\", args.externalId),\n )\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseScimIdentity\", args);\n },\n});\n\n/**\n * Permanently delete a SCIM identity record.\n *\n * Removes the `EnterpriseScimIdentity` document. This is typically called\n * when a SCIM DELETE request is received for a user or group resource.\n *\n * @param args.identityId - The document ID of the SCIM identity to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseScimIdentityDelete,\n * { identityId: scimIdentity._id },\n * );\n * ```\n */\nexport const enterpriseScimIdentityDelete = mutation({\n args: { identityId: v.id(\"EnterpriseScimIdentity\") },\n returns: v.null(),\n handler: async (ctx, { identityId }) => {\n await ctx.db.delete(identityId);\n return null;\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAa,6BAA6B,SAAS;CACjD,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,SAAS,EAAE,GAAG,QAAQ;EACtB,QAAQ;EACR,UAAU,EAAE,QAAQ;EACpB,WAAW,EAAE,QAAQ;EACrB,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;EACrC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC;EAC5B;CACD,SAAS,EAAE,GAAG,uBAAuB;CACrC,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,uBAAuB,CAC7B,UAAU,kBAAkB,QAC3B,IAAI,GAAG,gBAAgB,KAAK,aAAa,CAC1C,CACA,OAAO;AACV,MAAI,UAAU;AACZ,SAAM,IAAI,GAAG,MAAM,SAAS,KAAK,KAAK;AACtC,UAAO,SAAS;;AAElB,SAAO,MAAM,IAAI,GAAG,OAAO,wBAAwB,KAAK;;CAE3D,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,sCAAsC,MAAM;CACvD,MAAM,EAAE,cAAc,EAAE,GAAG,aAAa,EAAE;CAC1C,SAAS,EAAE,MAAM,0BAA0B,EAAE,MAAM,CAAC;CACpD,SAAS,OAAO,KAAK,EAAE,mBAAmB;AACxC,SAAO,MAAM,IAAI,GACd,MAAM,uBAAuB,CAC7B,UAAU,kBAAkB,QAAQ,IAAI,GAAG,gBAAgB,aAAa,CAAC,CACzE,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,qCAAqC,MAAM;CACtD,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE;CAC/B,SAAS,EAAE,MAAM,0BAA0B,EAAE,MAAM,CAAC;CACpD,SAAS,OAAO,KAAK,EAAE,gBAAgB;AACrC,SAAO,MAAM,IAAI,GACd,MAAM,uBAAuB,CAC7B,UAAU,eAAe,QAAQ,IAAI,GAAG,aAAa,UAAU,CAAC,CAChE,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,4BAA4B,MAAM;CAC7C,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,cAAc;EACd,YAAY,EAAE,QAAQ;EACvB;CACD,SAAS,EAAE,MAAM,4BAA4B,EAAE,MAAM,CAAC;CACtD,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GACd,MAAM,yBAAyB,CAC/B,UAAU,4CAA4C,QACrD,IACG,GAAG,gBAAgB,KAAK,aAAa,CACrC,GAAG,gBAAgB,KAAK,aAAa,CACrC,GAAG,cAAc,KAAK,WAAW,CACrC,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBF,MAAa,kCAAkC,MAAM;CACnD,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,4BAA4B,EAAE,MAAM,CAAC;CACtD,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,yBAAyB,CAC/B,UAAU,YAAY,QAAQ,IAAI,GAAG,UAAU,OAAO,CAAC,CACvD,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,+CAA+C,MAAM;CAChE,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,QAAQ,EAAE,GAAG,OAAO;EACrB;CACD,SAAS,EAAE,MAAM,4BAA4B,EAAE,MAAM,CAAC;CACtD,SAAS,OAAO,KAAK,EAAE,cAAc,aAAa;AAChD,SAAO,MAAM,IAAI,GACd,MAAM,yBAAyB,CAC/B,UAAU,0BAA0B,QACnC,IAAI,GAAG,gBAAgB,aAAa,CAAC,GAAG,UAAU,OAAO,CAC1D,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;AAuBF,MAAa,yCAAyC,MAAM;CAC1D,MAAM,EAAE,eAAe,EAAE,GAAG,QAAQ,EAAE;CACtC,SAAS,EAAE,MAAM,4BAA4B,EAAE,MAAM,CAAC;CACtD,SAAS,OAAO,KAAK,EAAE,oBAAoB;AACzC,SAAO,MAAM,IAAI,GACd,MAAM,yBAAyB,CAC/B,UAAU,oBAAoB,QAC7B,IAAI,GAAG,iBAAiB,cAAc,CACvC,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,yCAAyC,MAAM;CAC1D,MAAM,EAAE,cAAc,EAAE,GAAG,aAAa,EAAE;CAC1C,SAAS,EAAE,MAAM,2BAA2B;CAC5C,SAAS,OAAO,KAAK,EAAE,mBAAmB;AACxC,SAAO,MAAM,IAAI,GACd,MAAM,yBAAyB,CAC/B,UAAU,kBAAkB,QAAQ,IAAI,GAAG,gBAAgB,aAAa,CAAC,CACzE,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCF,MAAa,+BAA+B,SAAS;CACnD,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,SAAS,EAAE,GAAG,QAAQ;EACtB,cAAc;EACd,YAAY,EAAE,QAAQ;EACtB,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;EAChC,eAAe,EAAE,SAAS,EAAE,GAAG,QAAQ,CAAC;EACxC,mBAAmB,EAAE,SAAS,EAAE,QAAQ,CAAC;EACzC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC;EAC/B,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC;EACzB;CACD,SAAS,EAAE,GAAG,yBAAyB;CACvC,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,yBAAyB,CAC/B,UAAU,4CAA4C,QACrD,IACG,GAAG,gBAAgB,KAAK,aAAa,CACrC,GAAG,gBAAgB,KAAK,aAAa,CACrC,GAAG,cAAc,KAAK,WAAW,CACrC,CACA,OAAO;AACV,MAAI,UAAU;AACZ,SAAM,IAAI,GAAG,MAAM,SAAS,KAAK,KAAK;AACtC,UAAO,SAAS;;AAElB,SAAO,MAAM,IAAI,GAAG,OAAO,0BAA0B,KAAK;;CAE7D,CAAC;;;;;;;;;;;;;;;;;;AAmBF,MAAa,+BAA+B,SAAS;CACnD,MAAM,EAAE,YAAY,EAAE,GAAG,yBAAyB,EAAE;CACpD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,iBAAiB;AACtC,QAAM,IAAI,GAAG,OAAO,WAAW;AAC/B,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"scim.js","names":[],"sources":["../../../../src/component/public/enterprise/scim.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport {\n vEnterpriseScimConfigDoc,\n vEnterpriseScimIdentityDoc,\n vScimResourceType,\n vScimStatus,\n} from \"../../model\";\n\n/**\n * Create or update the SCIM provisioning configuration for an enterprise.\n *\n * If a SCIM config already exists for the given enterprise, all fields are\n * patched in place (useful for rotating the bearer token). Otherwise a new\n * config document is created. Only one SCIM config is allowed per enterprise.\n *\n * @param args.enterpriseId - The ID of the enterprise to configure SCIM for.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.status - The SCIM config lifecycle status: `\"draft\"`, `\"active\"`, or `\"disabled\"`.\n * @param args.basePath - The base URL path for the SCIM endpoint (e.g. `\"/scim/v2\"`).\n * @param args.tokenHash - A hash of the bearer token used to authenticate SCIM requests.\n * @param args.lastRotatedAt - An optional epoch timestamp (ms) recording when the token was last rotated.\n * @param args.extend - An optional arbitrary extension object for custom SCIM settings.\n * @returns The ID of the created or updated `EnterpriseScimConfig` document.\n *\n * @example\n * ```ts\n * const configId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseScimConfigUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * status: \"active\",\n * basePath: \"/scim/v2\",\n * tokenHash: \"sha256:abc123...\",\n * lastRotatedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const enterpriseScimConfigUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n status: vScimStatus,\n basePath: v.string(),\n tokenHash: v.string(),\n lastRotatedAt: v.optional(v.number()),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"EnterpriseScimConfig\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseScimConfig\")\n .withIndex(\"enterprise_id\", (idx) =>\n idx.eq(\"enterpriseId\", args.enterpriseId),\n )\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseScimConfig\", args);\n },\n});\n\n/**\n * Retrieve the SCIM configuration for a specific enterprise.\n *\n * Looks up the SCIM config document by enterprise ID using the\n * `enterprise_id` index. Returns `null` if SCIM has not been configured.\n *\n * @param args.enterpriseId - The ID of the enterprise whose SCIM config to retrieve.\n * @returns The SCIM configuration document, or `null` if not configured.\n *\n * @example\n * ```ts\n * const config = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimConfigGetByEnterprise,\n * { enterpriseId },\n * );\n * if (config) {\n * console.log(config.status, config.basePath);\n * }\n * ```\n */\nexport const enterpriseScimConfigGetByEnterprise = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.union(vEnterpriseScimConfigDoc, v.null()),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseScimConfig\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .first();\n },\n});\n\n/**\n * Look up a SCIM configuration by its bearer token hash.\n *\n * Used during SCIM request authentication to resolve which enterprise a\n * given bearer token belongs to. Returns `null` if no config matches.\n *\n * @param args.tokenHash - The hash of the bearer token from the incoming SCIM request.\n * @returns The matching SCIM configuration document, or `null` if not found.\n *\n * @example\n * ```ts\n * const config = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimConfigGetByTokenHash,\n * { tokenHash: \"sha256:abc123...\" },\n * );\n * if (config) {\n * console.log(\"Authenticated enterprise:\", config.enterpriseId);\n * }\n * ```\n */\nexport const enterpriseScimConfigGetByTokenHash = query({\n args: { tokenHash: v.string() },\n returns: v.union(vEnterpriseScimConfigDoc, v.null()),\n handler: async (ctx, { tokenHash }) => {\n return await ctx.db\n .query(\"EnterpriseScimConfig\")\n .withIndex(\"token_hash\", (idx) => idx.eq(\"tokenHash\", tokenHash))\n .first();\n },\n});\n\n/**\n * Retrieve a SCIM identity by enterprise, resource type, and external ID.\n *\n * Looks up a SCIM-provisioned identity using the composite index on\n * `(enterpriseId, resourceType, externalId)`. This is the primary lookup\n * used when processing incoming SCIM user or group operations.\n *\n * @param args.enterpriseId - The ID of the enterprise that owns the SCIM identity.\n * @param args.resourceType - The SCIM resource type: `\"user\"` or `\"group\"`.\n * @param args.externalId - The external identifier assigned by the identity provider.\n * @returns The SCIM identity document, or `null` if not found.\n *\n * @example\n * ```ts\n * const identity = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGet,\n * {\n * enterpriseId,\n * resourceType: \"user\",\n * externalId: \"okta-user-abc123\",\n * },\n * );\n * ```\n */\nexport const enterpriseScimIdentityGet = query({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n resourceType: vScimResourceType,\n externalId: v.string(),\n },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, args) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id_resource_type_external_id\", (idx) =>\n idx\n .eq(\"enterpriseId\", args.enterpriseId)\n .eq(\"resourceType\", args.resourceType)\n .eq(\"externalId\", args.externalId),\n )\n .first();\n },\n});\n\n/**\n * Retrieve the SCIM identity linked to a specific user.\n *\n * Looks up the first SCIM identity document associated with the given user ID\n * via the `user_id` index. Useful for checking whether a user was provisioned\n * through SCIM.\n *\n * @param args.userId - The document ID of the user whose SCIM identity to retrieve.\n * @returns The SCIM identity document, or `null` if the user has no SCIM identity.\n *\n * @example\n * ```ts\n * const scimIdentity = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGetByUser,\n * { userId },\n * );\n * if (scimIdentity) {\n * console.log(\"User provisioned via SCIM:\", scimIdentity.externalId);\n * }\n * ```\n */\nexport const enterpriseScimIdentityGetByUser = query({\n args: { userId: v.id(\"User\") },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"user_id\", (idx) => idx.eq(\"userId\", userId))\n .first();\n },\n});\n\n/**\n * Retrieve the SCIM identity for a specific user within a specific enterprise.\n *\n * Uses the composite `(enterpriseId, userId)` index to find the SCIM identity\n * that links a user to a particular enterprise. This is useful when a user may\n * belong to multiple enterprises.\n *\n * @param args.enterpriseId - The ID of the enterprise to scope the lookup to.\n * @param args.userId - The document ID of the user.\n * @returns The SCIM identity document, or `null` if not found.\n *\n * @example\n * ```ts\n * const identity = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGetByEnterpriseAndUser,\n * { enterpriseId, userId },\n * );\n * ```\n */\nexport const enterpriseScimIdentityGetByEnterpriseAndUser = query({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n userId: v.id(\"User\"),\n },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, { enterpriseId, userId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id_user_id\", (idx) =>\n idx.eq(\"enterpriseId\", enterpriseId).eq(\"userId\", userId),\n )\n .first();\n },\n});\n\n/**\n * Retrieve the SCIM identity that is mapped to a specific group.\n *\n * Looks up a SCIM identity by its `mappedGroupId` field. This is used when\n * a SCIM group resource has been mapped to an internal group, and you need\n * to find the corresponding SCIM identity record.\n *\n * @param args.mappedGroupId - The document ID of the internal group that a SCIM group is mapped to.\n * @returns The SCIM identity document, or `null` if no mapping exists.\n *\n * @example\n * ```ts\n * const scimGroup = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityGetByMappedGroup,\n * { mappedGroupId: teamGroupId },\n * );\n * if (scimGroup) {\n * console.log(\"SCIM external group ID:\", scimGroup.externalId);\n * }\n * ```\n */\nexport const enterpriseScimIdentityGetByMappedGroup = query({\n args: { mappedGroupId: v.id(\"Group\") },\n returns: v.union(vEnterpriseScimIdentityDoc, v.null()),\n handler: async (ctx, { mappedGroupId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"mapped_group_id\", (idx) =>\n idx.eq(\"mappedGroupId\", mappedGroupId),\n )\n .first();\n },\n});\n\n/**\n * List all SCIM identities belonging to a specific enterprise.\n *\n * Returns all `EnterpriseScimIdentity` documents for the given enterprise,\n * including both user and group resource types. Useful for displaying all\n * SCIM-provisioned resources or for bulk operations.\n *\n * @param args.enterpriseId - The ID of the enterprise whose SCIM identities to list.\n * @returns An array of SCIM identity documents.\n *\n * @example\n * ```ts\n * const identities = await ctx.runQuery(\n * components.auth.enterprise.enterpriseScimIdentityListByEnterprise,\n * { enterpriseId },\n * );\n * const users = identities.filter((i) => i.resourceType === \"user\");\n * const groups = identities.filter((i) => i.resourceType === \"group\");\n * ```\n */\nexport const enterpriseScimIdentityListByEnterprise = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.array(vEnterpriseScimIdentityDoc),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .collect();\n },\n});\n\n/**\n * Create or update a SCIM-provisioned identity record.\n *\n * If a SCIM identity with the same `(enterpriseId, resourceType, externalId)`\n * already exists, its fields are patched in place. Otherwise a new record is\n * created. This is the core upsert used by the SCIM provisioning handler to\n * sync users and groups from external identity providers.\n *\n * @param args.enterpriseId - The ID of the enterprise the identity belongs to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.resourceType - The SCIM resource type: `\"user\"` or `\"group\"`.\n * @param args.externalId - The external identifier assigned by the identity provider.\n * @param args.userId - An optional link to the internal user document (for user resources).\n * @param args.mappedGroupId - An optional link to an internal group document (for group resources).\n * @param args.lastProvisionedAt - An optional epoch timestamp (ms) of the last sync.\n * @param args.active - An optional flag indicating whether the identity is active.\n * @param args.raw - An optional raw SCIM payload stored for debugging or re-processing.\n * @returns The ID of the created or updated `EnterpriseScimIdentity` document.\n *\n * @example\n * ```ts\n * const identityId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseScimIdentityUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * resourceType: \"user\",\n * externalId: \"okta-user-abc123\",\n * userId,\n * active: true,\n * lastProvisionedAt: Date.now(),\n * raw: { schemas: [\"urn:ietf:params:scim:schemas:core:2.0:User\"], userName: \"jane@acme.com\" },\n * },\n * );\n * ```\n */\nexport const enterpriseScimIdentityUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n resourceType: vScimResourceType,\n externalId: v.string(),\n userId: v.optional(v.id(\"User\")),\n mappedGroupId: v.optional(v.id(\"Group\")),\n lastProvisionedAt: v.optional(v.number()),\n active: v.optional(v.boolean()),\n raw: v.optional(v.any()),\n },\n returns: v.id(\"EnterpriseScimIdentity\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseScimIdentity\")\n .withIndex(\"enterprise_id_resource_type_external_id\", (idx) =>\n idx\n .eq(\"enterpriseId\", args.enterpriseId)\n .eq(\"resourceType\", args.resourceType)\n .eq(\"externalId\", args.externalId),\n )\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseScimIdentity\", args);\n },\n});\n\n/**\n * Permanently delete a SCIM identity record.\n *\n * Removes the `EnterpriseScimIdentity` document. This is typically called\n * when a SCIM DELETE request is received for a user or group resource.\n *\n * @param args.identityId - The document ID of the SCIM identity to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseScimIdentityDelete,\n * { identityId: scimIdentity._id },\n * );\n * ```\n */\nexport const enterpriseScimIdentityDelete = mutation({\n args: { identityId: v.id(\"EnterpriseScimIdentity\") },\n returns: v.null(),\n handler: async (ctx, { identityId }) => {\n await ctx.db.delete(identityId);\n return null;\n },\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/secrets.ts"],"mappings":";;;;;;;;;;;AAiCA;;;;;AA8CA;;;;;AAkCA;;;;;;;;;;;cAhFa,sBAAA;;;;;;;;;;;;;;;;;;;;;;;cA8CA,mBAAA;;;;;;;;;;;;;;;;;;;cAkCA,sBAAA"}
1
+ {"version":3,"file":"secrets.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/secrets.ts"],"mappings":";;;;;;;;;;;AAkCA;;;;;AA8CA;;;;;AAkCA;;;;;;;;;;;cAhFa,sBAAA;;;;;;;;;;;;;;;;;;;;;;;cA8CA,mBAAA;;;;;;;;;;;;;;;;;;;cAkCA,sBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.js","names":[],"sources":["../../../../src/component/public/enterprise/secrets.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vEnterpriseSecretDoc, vEnterpriseSecretKind } from \"../../model\";\n\n/**\n * Create or update an encrypted secret for an enterprise.\n *\n * Stores a secret identified by the combination of `(enterpriseId, kind)`.\n * If a secret of the same kind already exists for the enterprise, it is\n * updated with the new ciphertext and timestamp. Otherwise a new secret\n * document is created. Only one secret per kind is allowed per enterprise.\n *\n * @param args.enterpriseId - The ID of the enterprise the secret belongs to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.kind - The type of secret being stored (e.g. `\"oidc_client_secret\"`).\n * @param args.ciphertext - The encrypted secret value.\n * @param args.updatedAt - Epoch timestamp (ms) when the secret was last updated.\n * @returns The ID of the created or updated `EnterpriseSecret` document.\n *\n * @example\n * ```ts\n * const secretId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseSecretUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * kind: \"oidc_client_secret\",\n * ciphertext: \"encrypted:aes256:...\",\n * updatedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const enterpriseSecretUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n kind: vEnterpriseSecretKind,\n ciphertext: v.string(),\n updatedAt: v.number(),\n },\n returns: v.id(\"EnterpriseSecret\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseSecret\")\n .withIndex(\"enterprise_id_kind\", (idx) =>\n idx.eq(\"enterpriseId\", args.enterpriseId).eq(\"kind\", args.kind),\n )\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseSecret\", args);\n },\n});\n\n/**\n * Retrieve an encrypted secret for an enterprise by kind.\n *\n * Looks up the secret using the composite `(enterpriseId, kind)` index.\n * Returns the full document including the ciphertext, or `null` if no secret\n * of that kind has been stored for the enterprise.\n *\n * @param args.enterpriseId - The ID of the enterprise whose secret to retrieve.\n * @param args.kind - The type of secret to look up (e.g. `\"oidc_client_secret\"`).\n * @returns The enterprise secret document, or `null` if not found.\n *\n * @example\n * ```ts\n * const secret = await ctx.runQuery(\n * components.auth.enterprise.enterpriseSecretGet,\n * { enterpriseId, kind: \"oidc_client_secret\" },\n * );\n * if (secret) {\n * const plaintext = decrypt(secret.ciphertext);\n * }\n * ```\n */\nexport const enterpriseSecretGet = query({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n kind: vEnterpriseSecretKind,\n },\n returns: v.union(vEnterpriseSecretDoc, v.null()),\n handler: async (ctx, { enterpriseId, kind }) => {\n return await ctx.db\n .query(\"EnterpriseSecret\")\n .withIndex(\"enterprise_id_kind\", (idx) =>\n idx.eq(\"enterpriseId\", enterpriseId).eq(\"kind\", kind),\n )\n .first();\n },\n});\n\n/**\n * Delete an encrypted secret for an enterprise by kind.\n *\n * Removes the secret document matching the `(enterpriseId, kind)` pair.\n * If no such secret exists, this is a no-op.\n *\n * @param args.enterpriseId - The ID of the enterprise whose secret to delete.\n * @param args.kind - The type of secret to remove (e.g. `\"oidc_client_secret\"`).\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseSecretDelete,\n * { enterpriseId, kind: \"oidc_client_secret\" },\n * );\n * ```\n */\nexport const enterpriseSecretDelete = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n kind: vEnterpriseSecretKind,\n },\n returns: v.null(),\n handler: async (ctx, { enterpriseId, kind }) => {\n const existing = await ctx.db\n .query(\"EnterpriseSecret\")\n .withIndex(\"enterprise_id_kind\", (idx) =>\n idx.eq(\"enterpriseId\", enterpriseId).eq(\"kind\", kind),\n )\n .first();\n if (existing) {\n await ctx.db.delete(existing._id);\n }\n return null;\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,SAAS,EAAE,GAAG,QAAQ;EACtB,MAAM;EACN,YAAY,EAAE,QAAQ;EACtB,WAAW,EAAE,QAAQ;EACtB;CACD,SAAS,EAAE,GAAG,mBAAmB;CACjC,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,mBAAmB,CACzB,UAAU,uBAAuB,QAChC,IAAI,GAAG,gBAAgB,KAAK,aAAa,CAAC,GAAG,QAAQ,KAAK,KAAK,CAChE,CACA,OAAO;AACV,MAAI,UAAU;AACZ,SAAM,IAAI,GAAG,MAAM,SAAS,KAAK,KAAK;AACtC,UAAO,SAAS;;AAElB,SAAO,MAAM,IAAI,GAAG,OAAO,oBAAoB,KAAK;;CAEvD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,sBAAsB,MAAM;CACvC,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,MAAM;EACP;CACD,SAAS,EAAE,MAAM,sBAAsB,EAAE,MAAM,CAAC;CAChD,SAAS,OAAO,KAAK,EAAE,cAAc,WAAW;AAC9C,SAAO,MAAM,IAAI,GACd,MAAM,mBAAmB,CACzB,UAAU,uBAAuB,QAChC,IAAI,GAAG,gBAAgB,aAAa,CAAC,GAAG,QAAQ,KAAK,CACtD,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;AAoBF,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,MAAM;EACP;CACD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,cAAc,WAAW;EAC9C,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,mBAAmB,CACzB,UAAU,uBAAuB,QAChC,IAAI,GAAG,gBAAgB,aAAa,CAAC,GAAG,QAAQ,KAAK,CACtD,CACA,OAAO;AACV,MAAI,SACF,OAAM,IAAI,GAAG,OAAO,SAAS,IAAI;AAEnC,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"secrets.js","names":[],"sources":["../../../../src/component/public/enterprise/secrets.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vEnterpriseSecretDoc, vEnterpriseSecretKind } from \"../../model\";\n\n/**\n * Create or update an encrypted secret for an enterprise.\n *\n * Stores a secret identified by the combination of `(enterpriseId, kind)`.\n * If a secret of the same kind already exists for the enterprise, it is\n * updated with the new ciphertext and timestamp. Otherwise a new secret\n * document is created. Only one secret per kind is allowed per enterprise.\n *\n * @param args.enterpriseId - The ID of the enterprise the secret belongs to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.kind - The type of secret being stored (e.g. `\"oidc_client_secret\"`).\n * @param args.ciphertext - The encrypted secret value.\n * @param args.updatedAt - Epoch timestamp (ms) when the secret was last updated.\n * @returns The ID of the created or updated `EnterpriseSecret` document.\n *\n * @example\n * ```ts\n * const secretId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseSecretUpsert,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * kind: \"oidc_client_secret\",\n * ciphertext: \"encrypted:aes256:...\",\n * updatedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const enterpriseSecretUpsert = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n kind: vEnterpriseSecretKind,\n ciphertext: v.string(),\n updatedAt: v.number(),\n },\n returns: v.id(\"EnterpriseSecret\"),\n handler: async (ctx, args) => {\n const existing = await ctx.db\n .query(\"EnterpriseSecret\")\n .withIndex(\"enterprise_id_kind\", (idx) =>\n idx.eq(\"enterpriseId\", args.enterpriseId).eq(\"kind\", args.kind),\n )\n .first();\n if (existing) {\n await ctx.db.patch(existing._id, args);\n return existing._id;\n }\n return await ctx.db.insert(\"EnterpriseSecret\", args);\n },\n});\n\n/**\n * Retrieve an encrypted secret for an enterprise by kind.\n *\n * Looks up the secret using the composite `(enterpriseId, kind)` index.\n * Returns the full document including the ciphertext, or `null` if no secret\n * of that kind has been stored for the enterprise.\n *\n * @param args.enterpriseId - The ID of the enterprise whose secret to retrieve.\n * @param args.kind - The type of secret to look up (e.g. `\"oidc_client_secret\"`).\n * @returns The enterprise secret document, or `null` if not found.\n *\n * @example\n * ```ts\n * const secret = await ctx.runQuery(\n * components.auth.enterprise.enterpriseSecretGet,\n * { enterpriseId, kind: \"oidc_client_secret\" },\n * );\n * if (secret) {\n * const plaintext = decrypt(secret.ciphertext);\n * }\n * ```\n */\nexport const enterpriseSecretGet = query({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n kind: vEnterpriseSecretKind,\n },\n returns: v.union(vEnterpriseSecretDoc, v.null()),\n handler: async (ctx, { enterpriseId, kind }) => {\n return await ctx.db\n .query(\"EnterpriseSecret\")\n .withIndex(\"enterprise_id_kind\", (idx) =>\n idx.eq(\"enterpriseId\", enterpriseId).eq(\"kind\", kind),\n )\n .first();\n },\n});\n\n/**\n * Delete an encrypted secret for an enterprise by kind.\n *\n * Removes the secret document matching the `(enterpriseId, kind)` pair.\n * If no such secret exists, this is a no-op.\n *\n * @param args.enterpriseId - The ID of the enterprise whose secret to delete.\n * @param args.kind - The type of secret to remove (e.g. `\"oidc_client_secret\"`).\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseSecretDelete,\n * { enterpriseId, kind: \"oidc_client_secret\" },\n * );\n * ```\n */\nexport const enterpriseSecretDelete = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n kind: vEnterpriseSecretKind,\n },\n returns: v.null(),\n handler: async (ctx, { enterpriseId, kind }) => {\n const existing = await ctx.db\n .query(\"EnterpriseSecret\")\n .withIndex(\"enterprise_id_kind\", (idx) =>\n idx.eq(\"enterpriseId\", enterpriseId).eq(\"kind\", kind),\n )\n .first();\n if (existing) {\n await ctx.db.delete(existing._id);\n }\n return null;\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,SAAS,EAAE,GAAG,QAAQ;EACtB,MAAM;EACN,YAAY,EAAE,QAAQ;EACtB,WAAW,EAAE,QAAQ;EACtB;CACD,SAAS,EAAE,GAAG,mBAAmB;CACjC,SAAS,OAAO,KAAK,SAAS;EAC5B,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,mBAAmB,CACzB,UAAU,uBAAuB,QAChC,IAAI,GAAG,gBAAgB,KAAK,aAAa,CAAC,GAAG,QAAQ,KAAK,KAAK,CAChE,CACA,OAAO;AACV,MAAI,UAAU;AACZ,SAAM,IAAI,GAAG,MAAM,SAAS,KAAK,KAAK;AACtC,UAAO,SAAS;;AAElB,SAAO,MAAM,IAAI,GAAG,OAAO,oBAAoB,KAAK;;CAEvD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,sBAAsB,MAAM;CACvC,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,MAAM;EACP;CACD,SAAS,EAAE,MAAM,sBAAsB,EAAE,MAAM,CAAC;CAChD,SAAS,OAAO,KAAK,EAAE,cAAc,WAAW;AAC9C,SAAO,MAAM,IAAI,GACd,MAAM,mBAAmB,CACzB,UAAU,uBAAuB,QAChC,IAAI,GAAG,gBAAgB,aAAa,CAAC,GAAG,QAAQ,KAAK,CACtD,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;AAoBF,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EACJ,cAAc,EAAE,GAAG,aAAa;EAChC,MAAM;EACP;CACD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,cAAc,WAAW;EAC9C,MAAM,WAAW,MAAM,IAAI,GACxB,MAAM,mBAAmB,CACzB,UAAU,uBAAuB,QAChC,IAAI,GAAG,gBAAgB,aAAa,CAAC,GAAG,QAAQ,KAAK,CACtD,CACA,OAAO;AACV,MAAI,SACF,OAAM,IAAI,GAAG,OAAO,SAAS,IAAI;AAEnC,SAAO;;CAEV,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/webhooks.ts"],"mappings":";;;;;;;;;;;;;;;;AAwCA;;;;;AAyCA;;;;;AA+BA;;;;;AAkCA;;;;cA1Ga,+BAAA;AAiJb;;;;;AAyCA;;;;;AAkCA;;;;;AAsCA;;;;;AAjHA,cAxGa,6BAAA;;;;;;;;;;;;;;;;;;;;;cA+BA,4BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCA,+BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuCA,gCAAA;;;;;;;;;;;;;;;;;;;;;;;cAyCA,kCAAA;;;;;;;;;;;;;;;;;;;;;;cAkCA,6BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA,8BAAA"}
1
+ {"version":3,"file":"webhooks.d.ts","names":[],"sources":["../../../../src/component/public/enterprise/webhooks.ts"],"mappings":";;;;;;;;;;;;;;;;AAyCA;;;;;AAyCA;;;;;AA+BA;;;;;AAkCA;;;;cA1Ga,+BAAA;AAiJb;;;;;AAyCA;;;;;AAkCA;;;;;AAsCA;;;;;AAjHA,cAxGa,6BAAA;;;;;;;;;;;;;;;;;;;;;cA+BA,4BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCA,+BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuCA,gCAAA;;;;;;;;;;;;;;;;;;;;;;;cAyCA,kCAAA;;;;;;;;;;;;;;;;;;;;;;cAkCA,6BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA,8BAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.js","names":[],"sources":["../../../../src/component/public/enterprise/webhooks.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport {\n vEnterpriseWebhookDeliveryDoc,\n vEnterpriseWebhookEndpointDoc,\n vWebhookEndpointStatus,\n} from \"../../model\";\n\n/**\n * Register a new webhook endpoint for an enterprise.\n *\n * Creates an `EnterpriseWebhookEndpoint` document with an initial failure\n * count of `0`. The endpoint status defaults to `\"active\"` when not\n * explicitly provided. Each endpoint subscribes to a set of event types\n * that determine which deliveries are sent to it.\n *\n * @param args.enterpriseId - The ID of the enterprise this endpoint belongs to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.url - The HTTPS URL where webhook payloads will be delivered.\n * @param args.status - An optional lifecycle status (`\"active\"`, `\"paused\"`, or `\"disabled\"`). Defaults to `\"active\"`.\n * @param args.secretHash - A hash of the signing secret used to verify delivery payloads.\n * @param args.subscriptions - An array of event type strings this endpoint subscribes to (e.g. `[\"user.login\", \"scim.provision\"]`).\n * @param args.createdByUserId - An optional ID of the user who created this endpoint.\n * @param args.extend - An optional arbitrary extension object for custom endpoint metadata.\n * @returns The ID of the newly created `EnterpriseWebhookEndpoint` document.\n *\n * @example\n * ```ts\n * const endpointId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookEndpointCreate,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * url: \"https://acme.com/webhooks/auth\",\n * secretHash: \"sha256:whsec_...\",\n * subscriptions: [\"user.login\", \"user.created\", \"scim.provision\"],\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookEndpointCreate = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n url: v.string(),\n status: v.optional(vWebhookEndpointStatus),\n secretHash: v.string(),\n subscriptions: v.array(v.string()),\n createdByUserId: v.optional(v.id(\"User\")),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"EnterpriseWebhookEndpoint\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"EnterpriseWebhookEndpoint\", {\n ...args,\n status: args.status ?? \"active\",\n failureCount: 0,\n });\n },\n});\n\n/**\n * List all webhook endpoints registered for an enterprise.\n *\n * Returns all `EnterpriseWebhookEndpoint` documents associated with the\n * given enterprise, regardless of status.\n *\n * @param args.enterpriseId - The ID of the enterprise whose webhook endpoints to list.\n * @returns An array of webhook endpoint documents.\n *\n * @example\n * ```ts\n * const endpoints = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookEndpointList,\n * { enterpriseId },\n * );\n * for (const ep of endpoints) {\n * console.log(ep.url, ep.status, ep.subscriptions);\n * }\n * ```\n */\nexport const enterpriseWebhookEndpointList = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.array(vEnterpriseWebhookEndpointDoc),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseWebhookEndpoint\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .collect();\n },\n});\n\n/**\n * Retrieve a single webhook endpoint by its document ID.\n *\n * Returns the full endpoint document if it exists, or `null` if no\n * endpoint is found with the given ID.\n *\n * @param args.endpointId - The document ID of the webhook endpoint to retrieve.\n * @returns The webhook endpoint document, or `null` if not found.\n *\n * @example\n * ```ts\n * const endpoint = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookEndpointGet,\n * { endpointId },\n * );\n * if (endpoint) {\n * console.log(endpoint.url, endpoint.failureCount);\n * }\n * ```\n */\nexport const enterpriseWebhookEndpointGet = query({\n args: { endpointId: v.id(\"EnterpriseWebhookEndpoint\") },\n returns: v.union(vEnterpriseWebhookEndpointDoc, v.null()),\n handler: async (ctx, { endpointId }) => {\n return await ctx.db.get(endpointId);\n },\n});\n\n/**\n * Partially update (patch) an existing webhook endpoint.\n *\n * Merges the provided `data` fields into the endpoint document. Only the\n * fields present in `data` are changed; all other fields are preserved.\n * Common updates include changing the URL, rotating the secret, updating\n * subscriptions, or changing the status.\n *\n * @param args.endpointId - The document ID of the webhook endpoint to update.\n * @param args.data - An object containing the fields to update (e.g. `{ url, status, subscriptions }`).\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookEndpointUpdate,\n * {\n * endpointId,\n * data: {\n * status: \"paused\",\n * subscriptions: [\"user.login\"],\n * },\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookEndpointUpdate = mutation({\n args: { endpointId: v.id(\"EnterpriseWebhookEndpoint\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { endpointId, data }) => {\n await ctx.db.patch(endpointId, data);\n return null;\n },\n});\n\n/**\n * Enqueue a webhook delivery for a specific endpoint.\n *\n * Creates a new `EnterpriseWebhookDelivery` document with an initial status\n * of `\"pending\"` and an attempt count of `0`. The delivery will be picked up\n * by the delivery worker once `nextAttemptAt` is reached.\n *\n * @param args.enterpriseId - The ID of the enterprise the delivery belongs to.\n * @param args.endpointId - The ID of the webhook endpoint this delivery targets.\n * @param args.auditEventId - An optional ID of the audit event that triggered this delivery.\n * @param args.eventType - The event type string describing the payload (e.g. `\"user.created\"`).\n * @param args.payload - The arbitrary JSON payload to deliver to the endpoint.\n * @param args.nextAttemptAt - Epoch timestamp (ms) when the delivery should first be attempted.\n * @returns The ID of the newly created `EnterpriseWebhookDelivery` document.\n *\n * @example\n * ```ts\n * const deliveryId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookDeliveryEnqueue,\n * {\n * enterpriseId,\n * endpointId,\n * auditEventId,\n * eventType: \"user.created\",\n * payload: { userId, email: \"jane@acme.com\" },\n * nextAttemptAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookDeliveryEnqueue = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n endpointId: v.id(\"EnterpriseWebhookEndpoint\"),\n auditEventId: v.optional(v.id(\"EnterpriseAuditEvent\")),\n eventType: v.string(),\n payload: v.any(),\n nextAttemptAt: v.number(),\n },\n returns: v.id(\"EnterpriseWebhookDelivery\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"EnterpriseWebhookDelivery\", {\n ...args,\n status: \"pending\",\n attemptCount: 0,\n });\n },\n});\n\n/**\n * List pending webhook deliveries that are ready to be attempted.\n *\n * Queries the `status_next_attempt_at` index for deliveries with status\n * `\"pending\"` whose `nextAttemptAt` is at or before the provided timestamp.\n * This is used by the delivery worker to find deliveries due for processing.\n *\n * @param args.now - The current epoch timestamp (ms) used as the cutoff for `nextAttemptAt`.\n * @param args.limit - Maximum number of deliveries to return (clamped between 1 and 100, defaults to 50).\n * @returns An array of webhook delivery documents ready for dispatch.\n *\n * @example\n * ```ts\n * const ready = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookDeliveryListReady,\n * { now: Date.now(), limit: 10 },\n * );\n * for (const delivery of ready) {\n * await dispatchWebhook(delivery);\n * }\n * ```\n */\nexport const enterpriseWebhookDeliveryListReady = query({\n args: { now: v.number(), limit: v.optional(v.number()) },\n returns: v.array(vEnterpriseWebhookDeliveryDoc),\n handler: async (ctx, { now, limit }) => {\n return await ctx.db\n .query(\"EnterpriseWebhookDelivery\")\n .withIndex(\"status_next_attempt_at\", (idx) =>\n idx.eq(\"status\", \"pending\").lte(\"nextAttemptAt\", now),\n )\n .take(Math.min(Math.max(limit ?? 50, 1), 100));\n },\n});\n\n/**\n * List webhook deliveries for a specific enterprise, ordered by most recent first.\n *\n * Returns deliveries in reverse chronological order, useful for displaying\n * delivery history in an admin dashboard. Includes deliveries of all statuses.\n *\n * @param args.enterpriseId - The ID of the enterprise whose deliveries to list.\n * @param args.limit - Maximum number of deliveries to return (clamped between 1 and 100, defaults to 50).\n * @returns An array of webhook delivery documents, most recent first.\n *\n * @example\n * ```ts\n * const deliveries = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookDeliveryList,\n * { enterpriseId, limit: 25 },\n * );\n * for (const d of deliveries) {\n * console.log(d.eventType, d.status, d.attemptCount);\n * }\n * ```\n */\nexport const enterpriseWebhookDeliveryList = query({\n args: { enterpriseId: v.id(\"Enterprise\"), limit: v.optional(v.number()) },\n returns: v.array(vEnterpriseWebhookDeliveryDoc),\n handler: async (ctx, { enterpriseId, limit }) => {\n return await ctx.db\n .query(\"EnterpriseWebhookDelivery\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .order(\"desc\")\n .take(Math.min(Math.max(limit ?? 50, 1), 100));\n },\n});\n\n/**\n * Partially update (patch) an existing webhook delivery record.\n *\n * Merges the provided `data` fields into the delivery document. This is\n * typically used by the delivery worker to update the delivery status,\n * increment the attempt count, record response codes, or schedule retry\n * timestamps after a delivery attempt.\n *\n * @param args.deliveryId - The document ID of the webhook delivery to update.\n * @param args.data - An object containing the fields to update (e.g. `{ status, attemptCount, nextAttemptAt }`).\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookDeliveryPatch,\n * {\n * deliveryId,\n * data: {\n * status: \"delivered\",\n * attemptCount: 1,\n * },\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookDeliveryPatch = mutation({\n args: { deliveryId: v.id(\"EnterpriseWebhookDelivery\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { deliveryId, data }) => {\n await ctx.db.patch(deliveryId, data);\n return null;\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"webhooks.js","names":[],"sources":["../../../../src/component/public/enterprise/webhooks.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport {\n vEnterpriseWebhookDeliveryDoc,\n vEnterpriseWebhookEndpointDoc,\n vWebhookEndpointStatus,\n} from \"../../model\";\n\n/**\n * Register a new webhook endpoint for an enterprise.\n *\n * Creates an `EnterpriseWebhookEndpoint` document with an initial failure\n * count of `0`. The endpoint status defaults to `\"active\"` when not\n * explicitly provided. Each endpoint subscribes to a set of event types\n * that determine which deliveries are sent to it.\n *\n * @param args.enterpriseId - The ID of the enterprise this endpoint belongs to.\n * @param args.groupId - The ID of the root group that owns the enterprise.\n * @param args.url - The HTTPS URL where webhook payloads will be delivered.\n * @param args.status - An optional lifecycle status (`\"active\"`, `\"paused\"`, or `\"disabled\"`). Defaults to `\"active\"`.\n * @param args.secretHash - A hash of the signing secret used to verify delivery payloads.\n * @param args.subscriptions - An array of event type strings this endpoint subscribes to (e.g. `[\"user.login\", \"scim.provision\"]`).\n * @param args.createdByUserId - An optional ID of the user who created this endpoint.\n * @param args.extend - An optional arbitrary extension object for custom endpoint metadata.\n * @returns The ID of the newly created `EnterpriseWebhookEndpoint` document.\n *\n * @example\n * ```ts\n * const endpointId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookEndpointCreate,\n * {\n * enterpriseId,\n * groupId: orgGroupId,\n * url: \"https://acme.com/webhooks/auth\",\n * secretHash: \"sha256:whsec_...\",\n * subscriptions: [\"user.login\", \"user.created\", \"scim.provision\"],\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookEndpointCreate = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n groupId: v.id(\"Group\"),\n url: v.string(),\n status: v.optional(vWebhookEndpointStatus),\n secretHash: v.string(),\n subscriptions: v.array(v.string()),\n createdByUserId: v.optional(v.id(\"User\")),\n extend: v.optional(v.any()),\n },\n returns: v.id(\"EnterpriseWebhookEndpoint\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"EnterpriseWebhookEndpoint\", {\n ...args,\n status: args.status ?? \"active\",\n failureCount: 0,\n });\n },\n});\n\n/**\n * List all webhook endpoints registered for an enterprise.\n *\n * Returns all `EnterpriseWebhookEndpoint` documents associated with the\n * given enterprise, regardless of status.\n *\n * @param args.enterpriseId - The ID of the enterprise whose webhook endpoints to list.\n * @returns An array of webhook endpoint documents.\n *\n * @example\n * ```ts\n * const endpoints = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookEndpointList,\n * { enterpriseId },\n * );\n * for (const ep of endpoints) {\n * console.log(ep.url, ep.status, ep.subscriptions);\n * }\n * ```\n */\nexport const enterpriseWebhookEndpointList = query({\n args: { enterpriseId: v.id(\"Enterprise\") },\n returns: v.array(vEnterpriseWebhookEndpointDoc),\n handler: async (ctx, { enterpriseId }) => {\n return await ctx.db\n .query(\"EnterpriseWebhookEndpoint\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .collect();\n },\n});\n\n/**\n * Retrieve a single webhook endpoint by its document ID.\n *\n * Returns the full endpoint document if it exists, or `null` if no\n * endpoint is found with the given ID.\n *\n * @param args.endpointId - The document ID of the webhook endpoint to retrieve.\n * @returns The webhook endpoint document, or `null` if not found.\n *\n * @example\n * ```ts\n * const endpoint = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookEndpointGet,\n * { endpointId },\n * );\n * if (endpoint) {\n * console.log(endpoint.url, endpoint.failureCount);\n * }\n * ```\n */\nexport const enterpriseWebhookEndpointGet = query({\n args: { endpointId: v.id(\"EnterpriseWebhookEndpoint\") },\n returns: v.union(vEnterpriseWebhookEndpointDoc, v.null()),\n handler: async (ctx, { endpointId }) => {\n return await ctx.db.get(endpointId);\n },\n});\n\n/**\n * Partially update (patch) an existing webhook endpoint.\n *\n * Merges the provided `data` fields into the endpoint document. Only the\n * fields present in `data` are changed; all other fields are preserved.\n * Common updates include changing the URL, rotating the secret, updating\n * subscriptions, or changing the status.\n *\n * @param args.endpointId - The document ID of the webhook endpoint to update.\n * @param args.data - An object containing the fields to update (e.g. `{ url, status, subscriptions }`).\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookEndpointUpdate,\n * {\n * endpointId,\n * data: {\n * status: \"paused\",\n * subscriptions: [\"user.login\"],\n * },\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookEndpointUpdate = mutation({\n args: { endpointId: v.id(\"EnterpriseWebhookEndpoint\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { endpointId, data }) => {\n await ctx.db.patch(endpointId, data);\n return null;\n },\n});\n\n/**\n * Enqueue a webhook delivery for a specific endpoint.\n *\n * Creates a new `EnterpriseWebhookDelivery` document with an initial status\n * of `\"pending\"` and an attempt count of `0`. The delivery will be picked up\n * by the delivery worker once `nextAttemptAt` is reached.\n *\n * @param args.enterpriseId - The ID of the enterprise the delivery belongs to.\n * @param args.endpointId - The ID of the webhook endpoint this delivery targets.\n * @param args.auditEventId - An optional ID of the audit event that triggered this delivery.\n * @param args.eventType - The event type string describing the payload (e.g. `\"user.created\"`).\n * @param args.payload - The arbitrary JSON payload to deliver to the endpoint.\n * @param args.nextAttemptAt - Epoch timestamp (ms) when the delivery should first be attempted.\n * @returns The ID of the newly created `EnterpriseWebhookDelivery` document.\n *\n * @example\n * ```ts\n * const deliveryId = await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookDeliveryEnqueue,\n * {\n * enterpriseId,\n * endpointId,\n * auditEventId,\n * eventType: \"user.created\",\n * payload: { userId, email: \"jane@acme.com\" },\n * nextAttemptAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookDeliveryEnqueue = mutation({\n args: {\n enterpriseId: v.id(\"Enterprise\"),\n endpointId: v.id(\"EnterpriseWebhookEndpoint\"),\n auditEventId: v.optional(v.id(\"EnterpriseAuditEvent\")),\n eventType: v.string(),\n payload: v.any(),\n nextAttemptAt: v.number(),\n },\n returns: v.id(\"EnterpriseWebhookDelivery\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"EnterpriseWebhookDelivery\", {\n ...args,\n status: \"pending\",\n attemptCount: 0,\n });\n },\n});\n\n/**\n * List pending webhook deliveries that are ready to be attempted.\n *\n * Queries the `status_next_attempt_at` index for deliveries with status\n * `\"pending\"` whose `nextAttemptAt` is at or before the provided timestamp.\n * This is used by the delivery worker to find deliveries due for processing.\n *\n * @param args.now - The current epoch timestamp (ms) used as the cutoff for `nextAttemptAt`.\n * @param args.limit - Maximum number of deliveries to return (clamped between 1 and 100, defaults to 50).\n * @returns An array of webhook delivery documents ready for dispatch.\n *\n * @example\n * ```ts\n * const ready = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookDeliveryListReady,\n * { now: Date.now(), limit: 10 },\n * );\n * for (const delivery of ready) {\n * await dispatchWebhook(delivery);\n * }\n * ```\n */\nexport const enterpriseWebhookDeliveryListReady = query({\n args: { now: v.number(), limit: v.optional(v.number()) },\n returns: v.array(vEnterpriseWebhookDeliveryDoc),\n handler: async (ctx, { now, limit }) => {\n return await ctx.db\n .query(\"EnterpriseWebhookDelivery\")\n .withIndex(\"status_next_attempt_at\", (idx) =>\n idx.eq(\"status\", \"pending\").lte(\"nextAttemptAt\", now),\n )\n .take(Math.min(Math.max(limit ?? 50, 1), 100));\n },\n});\n\n/**\n * List webhook deliveries for a specific enterprise, ordered by most recent first.\n *\n * Returns deliveries in reverse chronological order, useful for displaying\n * delivery history in an admin dashboard. Includes deliveries of all statuses.\n *\n * @param args.enterpriseId - The ID of the enterprise whose deliveries to list.\n * @param args.limit - Maximum number of deliveries to return (clamped between 1 and 100, defaults to 50).\n * @returns An array of webhook delivery documents, most recent first.\n *\n * @example\n * ```ts\n * const deliveries = await ctx.runQuery(\n * components.auth.enterprise.enterpriseWebhookDeliveryList,\n * { enterpriseId, limit: 25 },\n * );\n * for (const d of deliveries) {\n * console.log(d.eventType, d.status, d.attemptCount);\n * }\n * ```\n */\nexport const enterpriseWebhookDeliveryList = query({\n args: { enterpriseId: v.id(\"Enterprise\"), limit: v.optional(v.number()) },\n returns: v.array(vEnterpriseWebhookDeliveryDoc),\n handler: async (ctx, { enterpriseId, limit }) => {\n return await ctx.db\n .query(\"EnterpriseWebhookDelivery\")\n .withIndex(\"enterprise_id\", (idx) => idx.eq(\"enterpriseId\", enterpriseId))\n .order(\"desc\")\n .take(Math.min(Math.max(limit ?? 50, 1), 100));\n },\n});\n\n/**\n * Partially update (patch) an existing webhook delivery record.\n *\n * Merges the provided `data` fields into the delivery document. This is\n * typically used by the delivery worker to update the delivery status,\n * increment the attempt count, record response codes, or schedule retry\n * timestamps after a delivery attempt.\n *\n * @param args.deliveryId - The document ID of the webhook delivery to update.\n * @param args.data - An object containing the fields to update (e.g. `{ status, attemptCount, nextAttemptAt }`).\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.enterprise.enterpriseWebhookDeliveryPatch,\n * {\n * deliveryId,\n * data: {\n * status: \"delivered\",\n * attemptCount: 1,\n * },\n * },\n * );\n * ```\n */\nexport const enterpriseWebhookDeliveryPatch = mutation({\n args: { deliveryId: v.id(\"EnterpriseWebhookDelivery\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { deliveryId, data }) => {\n await ctx.db.patch(deliveryId, data);\n return null;\n },\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"devices.d.ts","names":[],"sources":["../../../../src/component/public/factors/devices.ts"],"mappings":";;;;;;;;;;;;;;AAoCA;;;;;AAoCA;;;;;AAoCA;;;;;AAuCA;;;;;AAwCA;cAvJa,YAAA;;;;AAmLb;;;;;;;;;;;;;;;;;;;cA/Ia,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuCA,eAAA;;;;;;;;;;;;;;;;;;;;;;;;cAwCA,sBAAA;;;;;;;;;;;;;;;;;;;;cA4BA,YAAA"}
1
+ {"version":3,"file":"devices.d.ts","names":[],"sources":["../../../../src/component/public/factors/devices.ts"],"mappings":";;;;;;;;;;;;;;AAqCA;;;;;AAoCA;;;;;AAoCA;;;;;AAuCA;;;;;AAwCA;cAvJa,YAAA;;;;AAmLb;;;;;;;;;;;;;;;;;;;cA/Ia,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuCA,eAAA;;;;;;;;;;;;;;;;;;;;;;;;cAwCA,sBAAA;;;;;;;;;;;;;;;;;;;;cA4BA,YAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"devices.js","names":[],"sources":["../../../../src/component/public/factors/devices.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vDeviceCodeDoc, vDeviceStatus } from \"../../model\";\n\n/**\n * Insert a new device authorization record into the `DeviceCode` table.\n *\n * Creates a pending device authorization entry as part of the OAuth 2.0\n * Device Authorization Grant (RFC 8628). The record tracks the hashed device\n * code, the human-readable user code, expiry, and polling interval.\n *\n * @param deviceCodeHash - SHA-256 hash of the device code issued to the client.\n * Only the hash is stored; the raw code is never persisted.\n * @param userCode - Short, human-readable code displayed to the end-user\n * so they can authorize the device on a separate screen.\n * @param expiresAt - Unix timestamp (in milliseconds) after which the device\n * authorization request is no longer valid.\n * @param interval - Minimum polling interval in seconds that the device client\n * must wait between token requests.\n * @param status - Initial status of the device authorization (e.g. `\"pending\"`).\n * @returns The `_id` of the newly created `DeviceCode` document.\n *\n * @example\n * ```ts\n * const deviceCodeId = await ctx.runMutation(\n * components.auth.factors.devices.deviceInsert,\n * {\n * deviceCodeHash: \"a1b2c3d4e5f6...\",\n * userCode: \"ABCD-1234\",\n * expiresAt: Date.now() + 10 * 60 * 1000,\n * interval: 5,\n * status: \"pending\",\n * },\n * );\n * ```\n */\nexport const deviceInsert = mutation({\n args: {\n deviceCodeHash: v.string(),\n userCode: v.string(),\n expiresAt: v.number(),\n interval: v.number(),\n status: vDeviceStatus,\n },\n returns: v.id(\"DeviceCode\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"DeviceCode\", args);\n },\n});\n\n/**\n * Look up a device authorization record by its hashed device code.\n *\n * Queries the `DeviceCode` table using the `device_code_hash` index.\n * This is the primary lookup used by the token endpoint when a device\n * client polls for authorization status.\n *\n * @param deviceCodeHash - SHA-256 hash of the device code to look up.\n * @returns The matching `DeviceCode` document, or `null` if no record\n * exists for the given hash.\n *\n * @example\n * ```ts\n * const deviceCode = await ctx.runQuery(\n * components.auth.factors.devices.deviceGetByCodeHash,\n * { deviceCodeHash: \"a1b2c3d4e5f6...\" },\n * );\n * if (deviceCode && deviceCode.status === \"authorized\") {\n * // Exchange for tokens\n * }\n * ```\n */\nexport const deviceGetByCodeHash = query({\n args: { deviceCodeHash: v.string() },\n returns: v.union(vDeviceCodeDoc, v.null()),\n handler: async (ctx, { deviceCodeHash }) => {\n return await ctx.db\n .query(\"DeviceCode\")\n .withIndex(\"device_code_hash\", (q) =>\n q.eq(\"deviceCodeHash\", deviceCodeHash),\n )\n .first();\n },\n});\n\n/**\n * Look up a pending device authorization by its user-facing code.\n *\n * Queries the `DeviceCode` table using the `user_code_status` compound index,\n * filtering to only `\"pending\"` records. This is called when an authenticated\n * user enters the code shown on the device to approve the authorization.\n *\n * @param userCode - The short, human-readable code the user typed in\n * (e.g. `\"ABCD-1234\"`).\n * @returns The matching pending `DeviceCode` document, or `null` if no\n * pending authorization exists for the given user code.\n *\n * @example\n * ```ts\n * const pending = await ctx.runQuery(\n * components.auth.factors.devices.deviceGetByUserCode,\n * { userCode: \"ABCD-1234\" },\n * );\n * if (pending === null) {\n * throw new Error(\"Invalid or expired user code\");\n * }\n * ```\n */\nexport const deviceGetByUserCode = query({\n args: { userCode: v.string() },\n returns: v.union(vDeviceCodeDoc, v.null()),\n handler: async (ctx, { userCode }) => {\n return await ctx.db\n .query(\"DeviceCode\")\n .withIndex(\"user_code_status\", (q) =>\n q.eq(\"userCode\", userCode).eq(\"status\", \"pending\"),\n )\n .first();\n },\n});\n\n/**\n * Authorize a device code by linking it to a user and session.\n *\n * Transitions the device authorization status from `\"pending\"` to\n * `\"authorized\"` and associates it with the approving user and their\n * active session. After this mutation, the next poll from the device\n * client will succeed and tokens can be issued.\n *\n * @param deviceId - The `_id` of the `DeviceCode` document to authorize.\n * @param userId - The `_id` of the `User` who approved the device request.\n * @param sessionId - The `_id` of the `Session` associated with the\n * approving user's current login.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.devices.deviceAuthorize,\n * {\n * deviceId: pending._id,\n * userId: currentUser._id,\n * sessionId: currentSession._id,\n * },\n * );\n * ```\n */\nexport const deviceAuthorize = mutation({\n args: {\n deviceId: v.id(\"DeviceCode\"),\n userId: v.id(\"User\"),\n sessionId: v.id(\"Session\"),\n },\n returns: v.null(),\n handler: async (ctx, { deviceId, userId, sessionId }) => {\n await ctx.db.patch(\"DeviceCode\", deviceId, {\n status: \"authorized\",\n userId,\n sessionId,\n });\n return null;\n },\n});\n\n/**\n * Update the last-polled timestamp on a device authorization record.\n *\n * Called each time the device client polls the token endpoint. The\n * timestamp is used to enforce the minimum polling interval and to\n * detect slow-polling violations per RFC 8628.\n *\n * @param deviceId - The `_id` of the `DeviceCode` document to update.\n * @param lastPolledAt - Unix timestamp (in milliseconds) of the most\n * recent poll request from the device client.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.devices.deviceUpdateLastPolled,\n * {\n * deviceId: deviceCode._id,\n * lastPolledAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const deviceUpdateLastPolled = mutation({\n args: { deviceId: v.id(\"DeviceCode\"), lastPolledAt: v.number() },\n returns: v.null(),\n handler: async (ctx, { deviceId, lastPolledAt }) => {\n await ctx.db.patch(\"DeviceCode\", deviceId, { lastPolledAt });\n return null;\n },\n});\n\n/**\n * Delete a device authorization record from the `DeviceCode` table.\n *\n * Permanently removes the device code entry. This should be called after\n * the device authorization has been successfully exchanged for tokens, or\n * when the authorization has expired and needs to be cleaned up.\n *\n * @param deviceId - The `_id` of the `DeviceCode` document to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // Clean up after successful token exchange\n * await ctx.runMutation(\n * components.auth.factors.devices.deviceDelete,\n * { deviceId: deviceCode._id },\n * );\n * ```\n */\nexport const deviceDelete = mutation({\n args: { deviceId: v.id(\"DeviceCode\") },\n returns: v.null(),\n handler: async (ctx, { deviceId }) => {\n await ctx.db.delete(\"DeviceCode\", deviceId);\n return null;\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,eAAe,SAAS;CACnC,MAAM;EACJ,gBAAgB,EAAE,QAAQ;EAC1B,UAAU,EAAE,QAAQ;EACpB,WAAW,EAAE,QAAQ;EACrB,UAAU,EAAE,QAAQ;EACpB,QAAQ;EACT;CACD,SAAS,EAAE,GAAG,aAAa;CAC3B,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,cAAc,KAAK;;CAEjD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,sBAAsB,MAAM;CACvC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE;CACpC,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,qBAAqB;AAC1C,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,qBAAqB,MAC9B,EAAE,GAAG,kBAAkB,eAAe,CACvC,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,sBAAsB,MAAM;CACvC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE;CAC9B,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,eAAe;AACpC,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,qBAAqB,MAC9B,EAAE,GAAG,YAAY,SAAS,CAAC,GAAG,UAAU,UAAU,CACnD,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BF,MAAa,kBAAkB,SAAS;CACtC,MAAM;EACJ,UAAU,EAAE,GAAG,aAAa;EAC5B,QAAQ,EAAE,GAAG,OAAO;EACpB,WAAW,EAAE,GAAG,UAAU;EAC3B;CACD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,UAAU,QAAQ,gBAAgB;AACvD,QAAM,IAAI,GAAG,MAAM,cAAc,UAAU;GACzC,QAAQ;GACR;GACA;GACD,CAAC;AACF,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EAAE,UAAU,EAAE,GAAG,aAAa;EAAE,cAAc,EAAE,QAAQ;EAAE;CAChE,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,UAAU,mBAAmB;AAClD,QAAM,IAAI,GAAG,MAAM,cAAc,UAAU,EAAE,cAAc,CAAC;AAC5D,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,eAAe,SAAS;CACnC,MAAM,EAAE,UAAU,EAAE,GAAG,aAAa,EAAE;CACtC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,eAAe;AACpC,QAAM,IAAI,GAAG,OAAO,cAAc,SAAS;AAC3C,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"devices.js","names":[],"sources":["../../../../src/component/public/factors/devices.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vDeviceCodeDoc, vDeviceStatus } from \"../../model\";\n\n/**\n * Insert a new device authorization record into the `DeviceCode` table.\n *\n * Creates a pending device authorization entry as part of the OAuth 2.0\n * Device Authorization Grant (RFC 8628). The record tracks the hashed device\n * code, the human-readable user code, expiry, and polling interval.\n *\n * @param deviceCodeHash - SHA-256 hash of the device code issued to the client.\n * Only the hash is stored; the raw code is never persisted.\n * @param userCode - Short, human-readable code displayed to the end-user\n * so they can authorize the device on a separate screen.\n * @param expiresAt - Unix timestamp (in milliseconds) after which the device\n * authorization request is no longer valid.\n * @param interval - Minimum polling interval in seconds that the device client\n * must wait between token requests.\n * @param status - Initial status of the device authorization (e.g. `\"pending\"`).\n * @returns The `_id` of the newly created `DeviceCode` document.\n *\n * @example\n * ```ts\n * const deviceCodeId = await ctx.runMutation(\n * components.auth.factors.devices.deviceInsert,\n * {\n * deviceCodeHash: \"a1b2c3d4e5f6...\",\n * userCode: \"ABCD-1234\",\n * expiresAt: Date.now() + 10 * 60 * 1000,\n * interval: 5,\n * status: \"pending\",\n * },\n * );\n * ```\n */\nexport const deviceInsert = mutation({\n args: {\n deviceCodeHash: v.string(),\n userCode: v.string(),\n expiresAt: v.number(),\n interval: v.number(),\n status: vDeviceStatus,\n },\n returns: v.id(\"DeviceCode\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"DeviceCode\", args);\n },\n});\n\n/**\n * Look up a device authorization record by its hashed device code.\n *\n * Queries the `DeviceCode` table using the `device_code_hash` index.\n * This is the primary lookup used by the token endpoint when a device\n * client polls for authorization status.\n *\n * @param deviceCodeHash - SHA-256 hash of the device code to look up.\n * @returns The matching `DeviceCode` document, or `null` if no record\n * exists for the given hash.\n *\n * @example\n * ```ts\n * const deviceCode = await ctx.runQuery(\n * components.auth.factors.devices.deviceGetByCodeHash,\n * { deviceCodeHash: \"a1b2c3d4e5f6...\" },\n * );\n * if (deviceCode && deviceCode.status === \"authorized\") {\n * // Exchange for tokens\n * }\n * ```\n */\nexport const deviceGetByCodeHash = query({\n args: { deviceCodeHash: v.string() },\n returns: v.union(vDeviceCodeDoc, v.null()),\n handler: async (ctx, { deviceCodeHash }) => {\n return await ctx.db\n .query(\"DeviceCode\")\n .withIndex(\"device_code_hash\", (q) =>\n q.eq(\"deviceCodeHash\", deviceCodeHash),\n )\n .first();\n },\n});\n\n/**\n * Look up a pending device authorization by its user-facing code.\n *\n * Queries the `DeviceCode` table using the `user_code_status` compound index,\n * filtering to only `\"pending\"` records. This is called when an authenticated\n * user enters the code shown on the device to approve the authorization.\n *\n * @param userCode - The short, human-readable code the user typed in\n * (e.g. `\"ABCD-1234\"`).\n * @returns The matching pending `DeviceCode` document, or `null` if no\n * pending authorization exists for the given user code.\n *\n * @example\n * ```ts\n * const pending = await ctx.runQuery(\n * components.auth.factors.devices.deviceGetByUserCode,\n * { userCode: \"ABCD-1234\" },\n * );\n * if (pending === null) {\n * throw new Error(\"Invalid or expired user code\");\n * }\n * ```\n */\nexport const deviceGetByUserCode = query({\n args: { userCode: v.string() },\n returns: v.union(vDeviceCodeDoc, v.null()),\n handler: async (ctx, { userCode }) => {\n return await ctx.db\n .query(\"DeviceCode\")\n .withIndex(\"user_code_status\", (q) =>\n q.eq(\"userCode\", userCode).eq(\"status\", \"pending\"),\n )\n .first();\n },\n});\n\n/**\n * Authorize a device code by linking it to a user and session.\n *\n * Transitions the device authorization status from `\"pending\"` to\n * `\"authorized\"` and associates it with the approving user and their\n * active session. After this mutation, the next poll from the device\n * client will succeed and tokens can be issued.\n *\n * @param deviceId - The `_id` of the `DeviceCode` document to authorize.\n * @param userId - The `_id` of the `User` who approved the device request.\n * @param sessionId - The `_id` of the `Session` associated with the\n * approving user's current login.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.devices.deviceAuthorize,\n * {\n * deviceId: pending._id,\n * userId: currentUser._id,\n * sessionId: currentSession._id,\n * },\n * );\n * ```\n */\nexport const deviceAuthorize = mutation({\n args: {\n deviceId: v.id(\"DeviceCode\"),\n userId: v.id(\"User\"),\n sessionId: v.id(\"Session\"),\n },\n returns: v.null(),\n handler: async (ctx, { deviceId, userId, sessionId }) => {\n await ctx.db.patch(\"DeviceCode\", deviceId, {\n status: \"authorized\",\n userId,\n sessionId,\n });\n return null;\n },\n});\n\n/**\n * Update the last-polled timestamp on a device authorization record.\n *\n * Called each time the device client polls the token endpoint. The\n * timestamp is used to enforce the minimum polling interval and to\n * detect slow-polling violations per RFC 8628.\n *\n * @param deviceId - The `_id` of the `DeviceCode` document to update.\n * @param lastPolledAt - Unix timestamp (in milliseconds) of the most\n * recent poll request from the device client.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.devices.deviceUpdateLastPolled,\n * {\n * deviceId: deviceCode._id,\n * lastPolledAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const deviceUpdateLastPolled = mutation({\n args: { deviceId: v.id(\"DeviceCode\"), lastPolledAt: v.number() },\n returns: v.null(),\n handler: async (ctx, { deviceId, lastPolledAt }) => {\n await ctx.db.patch(\"DeviceCode\", deviceId, { lastPolledAt });\n return null;\n },\n});\n\n/**\n * Delete a device authorization record from the `DeviceCode` table.\n *\n * Permanently removes the device code entry. This should be called after\n * the device authorization has been successfully exchanged for tokens, or\n * when the authorization has expired and needs to be cleaned up.\n *\n * @param deviceId - The `_id` of the `DeviceCode` document to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // Clean up after successful token exchange\n * await ctx.runMutation(\n * components.auth.factors.devices.deviceDelete,\n * { deviceId: deviceCode._id },\n * );\n * ```\n */\nexport const deviceDelete = mutation({\n args: { deviceId: v.id(\"DeviceCode\") },\n returns: v.null(),\n handler: async (ctx, { deviceId }) => {\n await ctx.db.delete(\"DeviceCode\", deviceId);\n return null;\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAa,eAAe,SAAS;CACnC,MAAM;EACJ,gBAAgB,EAAE,QAAQ;EAC1B,UAAU,EAAE,QAAQ;EACpB,WAAW,EAAE,QAAQ;EACrB,UAAU,EAAE,QAAQ;EACpB,QAAQ;EACT;CACD,SAAS,EAAE,GAAG,aAAa;CAC3B,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,cAAc,KAAK;;CAEjD,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBF,MAAa,sBAAsB,MAAM;CACvC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE;CACpC,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,qBAAqB;AAC1C,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,qBAAqB,MAC9B,EAAE,GAAG,kBAAkB,eAAe,CACvC,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,sBAAsB,MAAM;CACvC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE;CAC9B,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,eAAe;AACpC,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,qBAAqB,MAC9B,EAAE,GAAG,YAAY,SAAS,CAAC,GAAG,UAAU,UAAU,CACnD,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BF,MAAa,kBAAkB,SAAS;CACtC,MAAM;EACJ,UAAU,EAAE,GAAG,aAAa;EAC5B,QAAQ,EAAE,GAAG,OAAO;EACpB,WAAW,EAAE,GAAG,UAAU;EAC3B;CACD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,UAAU,QAAQ,gBAAgB;AACvD,QAAM,IAAI,GAAG,MAAM,cAAc,UAAU;GACzC,QAAQ;GACR;GACA;GACD,CAAC;AACF,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,yBAAyB,SAAS;CAC7C,MAAM;EAAE,UAAU,EAAE,GAAG,aAAa;EAAE,cAAc,EAAE,QAAQ;EAAE;CAChE,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,UAAU,mBAAmB;AAClD,QAAM,IAAI,GAAG,MAAM,cAAc,UAAU,EAAE,cAAc,CAAC;AAC5D,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;AAqBF,MAAa,eAAe,SAAS;CACnC,MAAM,EAAE,UAAU,EAAE,GAAG,aAAa,EAAE;CACtC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,eAAe;AACpC,QAAM,IAAI,GAAG,OAAO,cAAc,SAAS;AAC3C,SAAO;;CAEV,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"passkeys.d.ts","names":[],"sources":["../../../../src/component/public/factors/passkeys.ts"],"mappings":";;;;;;;;;;;;;;AAoDA;;;;;AA0CA;;;;;AAmCA;;;;;AAsCA;;;;;AAoCA;;;;;AA2BA;;;;;;;;;;;;cAlLa,aAAA;;;;;;;;;;;;;;;;;;;;;;;;cA0CA,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;cAmCA,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,iBAAA;;;;;;;;;;;;;;;;;;;cA2BA,aAAA"}
1
+ {"version":3,"file":"passkeys.d.ts","names":[],"sources":["../../../../src/component/public/factors/passkeys.ts"],"mappings":";;;;;;;;;;;;;;AAqDA;;;;;AA0CA;;;;;AAmCA;;;;;AAsCA;;;;;AAoCA;;;;;AA2BA;;;;;;;;;;;;cAlLa,aAAA;;;;;;;;;;;;;;;;;;;;;;;;cA0CA,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;cAmCA,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,iBAAA;;;;;;;;;;;;;;;;;;;cA2BA,aAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"passkeys.js","names":[],"sources":["../../../../src/component/public/factors/passkeys.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vPasskeyDoc } from \"../../model\";\n\n/**\n * Store a new WebAuthn passkey credential for a user.\n *\n * Persists the public key material and metadata returned by the browser's\n * `navigator.credentials.create()` call after a successful registration\n * ceremony. Each passkey is tied to a single user.\n *\n * @param userId - The `_id` of the `User` who owns this passkey.\n * @param credentialId - Base64url-encoded credential identifier assigned\n * by the authenticator; used to look up the key during authentication.\n * @param publicKey - Raw public key bytes (COSE format) for signature\n * verification.\n * @param algorithm - COSE algorithm identifier (e.g. `-7` for ES256,\n * `-257` for RS256).\n * @param counter - Signature counter reported by the authenticator at\n * registration time; used to detect cloned credentials.\n * @param transports - Optional list of transport hints (e.g.\n * `[\"usb\", \"ble\", \"nfc\", \"internal\"]`) to help the browser select\n * the correct authenticator.\n * @param deviceType - Authenticator attachment type (e.g.\n * `\"singleDevice\"` or `\"multiDevice\"`).\n * @param backedUp - Whether the credential is backed up (synced) by the\n * authenticator platform.\n * @param name - Optional human-readable label for the passkey\n * (e.g. `\"MacBook Pro Touch ID\"`).\n * @param createdAt - Unix timestamp (in milliseconds) when the passkey\n * was registered.\n * @returns The `_id` of the newly created `Passkey` document.\n *\n * @example\n * ```ts\n * const passkeyId = await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyInsert,\n * {\n * userId: user._id,\n * credentialId: \"dGVzdC1jcmVkZW50aWFs\",\n * publicKey: publicKeyBytes,\n * algorithm: -7,\n * counter: 0,\n * transports: [\"internal\"],\n * deviceType: \"multiDevice\",\n * backedUp: true,\n * name: \"MacBook Pro Touch ID\",\n * createdAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const passkeyInsert = mutation({\n args: {\n userId: v.id(\"User\"),\n credentialId: v.string(),\n publicKey: v.bytes(),\n algorithm: v.number(),\n counter: v.number(),\n transports: v.optional(v.array(v.string())),\n deviceType: v.string(),\n backedUp: v.boolean(),\n name: v.optional(v.string()),\n createdAt: v.number(),\n },\n returns: v.id(\"Passkey\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"Passkey\", args);\n },\n});\n\n/**\n * Look up a passkey by its credential ID.\n *\n * Queries the `Passkey` table using the `credential_id` unique index.\n * This is the primary lookup during a WebAuthn authentication ceremony:\n * the authenticator provides a credential ID, and this function retrieves\n * the corresponding public key and counter for signature verification.\n *\n * @param credentialId - Base64url-encoded credential identifier to search for.\n * @returns The matching `Passkey` document, or `null` if no passkey exists\n * with the given credential ID.\n *\n * @example\n * ```ts\n * const passkey = await ctx.runQuery(\n * components.auth.factors.passkeys.passkeyGetByCredentialId,\n * { credentialId: \"dGVzdC1jcmVkZW50aWFs\" },\n * );\n * if (passkey === null) {\n * throw new Error(\"Unknown credential\");\n * }\n * ```\n */\nexport const passkeyGetByCredentialId = query({\n args: { credentialId: v.string() },\n returns: v.union(vPasskeyDoc, v.null()),\n handler: async (ctx, { credentialId }) => {\n return await ctx.db\n .query(\"Passkey\")\n .withIndex(\"credential_id\", (q) => q.eq(\"credentialId\", credentialId))\n .unique();\n },\n});\n\n/**\n * List all passkeys registered to a user.\n *\n * Retrieves every `Passkey` document associated with the given user via\n * the `user_id` index. Useful for displaying a user's registered\n * authenticators in a settings page, or for building the\n * `allowCredentials` list during a WebAuthn authentication ceremony.\n *\n * @param userId - The `_id` of the `User` whose passkeys to retrieve.\n * @returns An array of `Passkey` documents. Returns an empty array if the\n * user has no registered passkeys.\n *\n * @example\n * ```ts\n * const passkeys = await ctx.runQuery(\n * components.auth.factors.passkeys.passkeyListByUserId,\n * { userId: user._id },\n * );\n * // Display each passkey's name and creation date\n * for (const pk of passkeys) {\n * console.log(pk.name, new Date(pk.createdAt));\n * }\n * ```\n */\nexport const passkeyListByUserId = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vPasskeyDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"Passkey\")\n .withIndex(\"user_id\", (q) => q.eq(\"userId\", userId))\n .collect();\n },\n});\n\n/**\n * Update a passkey's signature counter and last-used timestamp after\n * a successful authentication.\n *\n * After verifying a WebAuthn assertion, the relying party must persist\n * the new counter value reported by the authenticator. A counter that\n * does not increase may indicate a cloned credential.\n *\n * @param passkeyId - The `_id` of the `Passkey` document to update.\n * @param counter - The new signature counter value returned by the\n * authenticator in the assertion response.\n * @param lastUsedAt - Unix timestamp (in milliseconds) recording when\n * this passkey was most recently used to authenticate.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyUpdateCounter,\n * {\n * passkeyId: passkey._id,\n * counter: assertionResponse.counter,\n * lastUsedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const passkeyUpdateCounter = mutation({\n args: {\n passkeyId: v.id(\"Passkey\"),\n counter: v.number(),\n lastUsedAt: v.number(),\n },\n returns: v.null(),\n handler: async (ctx, { passkeyId, counter, lastUsedAt }) => {\n await ctx.db.patch(\"Passkey\", passkeyId, { counter, lastUsedAt });\n return null;\n },\n});\n\n/**\n * Update a passkey's metadata fields.\n *\n * Performs a partial patch on the `Passkey` document. Typically used to\n * rename a passkey (e.g. from `\"Security Key\"` to `\"YubiKey 5C\"`), but\n * can update any mutable fields via the `data` argument.\n *\n * @param passkeyId - The `_id` of the `Passkey` document to update.\n * @param data - An object containing the fields to patch. Commonly\n * includes `{ name: \"New Label\" }`, but accepts any valid passkey fields.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyUpdateMeta,\n * {\n * passkeyId: passkey._id,\n * data: { name: \"YubiKey 5C NFC\" },\n * },\n * );\n * ```\n */\nexport const passkeyUpdateMeta = mutation({\n args: { passkeyId: v.id(\"Passkey\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { passkeyId, data }) => {\n await ctx.db.patch(\"Passkey\", passkeyId, data);\n return null;\n },\n});\n\n/**\n * Delete a passkey credential from the `Passkey` table.\n *\n * Permanently removes the passkey record. After deletion the credential\n * can no longer be used for authentication. Typically called from a\n * user's security settings when they want to unregister an authenticator.\n *\n * @param passkeyId - The `_id` of the `Passkey` document to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyDelete,\n * { passkeyId: passkey._id },\n * );\n * ```\n */\nexport const passkeyDelete = mutation({\n args: { passkeyId: v.id(\"Passkey\") },\n returns: v.null(),\n handler: async (ctx, { passkeyId }) => {\n await ctx.db.delete(\"Passkey\", passkeyId);\n return null;\n },\n});\n\n// ============================================================================\n// TOTP Two-Factor Authentication\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,gBAAgB,SAAS;CACpC,MAAM;EACJ,QAAQ,EAAE,GAAG,OAAO;EACpB,cAAc,EAAE,QAAQ;EACxB,WAAW,EAAE,OAAO;EACpB,WAAW,EAAE,QAAQ;EACrB,SAAS,EAAE,QAAQ;EACnB,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;EAC3C,YAAY,EAAE,QAAQ;EACtB,UAAU,EAAE,SAAS;EACrB,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC5B,WAAW,EAAE,QAAQ;EACtB;CACD,SAAS,EAAE,GAAG,UAAU;CACxB,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,WAAW,KAAK;;CAE9C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,2BAA2B,MAAM;CAC5C,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE;CAClC,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,mBAAmB;AACxC,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,kBAAkB,MAAM,EAAE,GAAG,gBAAgB,aAAa,CAAC,CACrE,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,sBAAsB,MAAM;CACvC,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,OAAO,CAAC,CACnD,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BF,MAAa,uBAAuB,SAAS;CAC3C,MAAM;EACJ,WAAW,EAAE,GAAG,UAAU;EAC1B,SAAS,EAAE,QAAQ;EACnB,YAAY,EAAE,QAAQ;EACvB;CACD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,WAAW,SAAS,iBAAiB;AAC1D,QAAM,IAAI,GAAG,MAAM,WAAW,WAAW;GAAE;GAAS;GAAY,CAAC;AACjE,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,oBAAoB,SAAS;CACxC,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;;;;;;;;;;;;;;;;;;;AAoBF,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":"passkeys.js","names":[],"sources":["../../../../src/component/public/factors/passkeys.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vPasskeyDoc } from \"../../model\";\n\n/**\n * Store a new WebAuthn passkey credential for a user.\n *\n * Persists the public key material and metadata returned by the browser's\n * `navigator.credentials.create()` call after a successful registration\n * ceremony. Each passkey is tied to a single user.\n *\n * @param userId - The `_id` of the `User` who owns this passkey.\n * @param credentialId - Base64url-encoded credential identifier assigned\n * by the authenticator; used to look up the key during authentication.\n * @param publicKey - Raw public key bytes (COSE format) for signature\n * verification.\n * @param algorithm - COSE algorithm identifier (e.g. `-7` for ES256,\n * `-257` for RS256).\n * @param counter - Signature counter reported by the authenticator at\n * registration time; used to detect cloned credentials.\n * @param transports - Optional list of transport hints (e.g.\n * `[\"usb\", \"ble\", \"nfc\", \"internal\"]`) to help the browser select\n * the correct authenticator.\n * @param deviceType - Authenticator attachment type (e.g.\n * `\"singleDevice\"` or `\"multiDevice\"`).\n * @param backedUp - Whether the credential is backed up (synced) by the\n * authenticator platform.\n * @param name - Optional human-readable label for the passkey\n * (e.g. `\"MacBook Pro Touch ID\"`).\n * @param createdAt - Unix timestamp (in milliseconds) when the passkey\n * was registered.\n * @returns The `_id` of the newly created `Passkey` document.\n *\n * @example\n * ```ts\n * const passkeyId = await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyInsert,\n * {\n * userId: user._id,\n * credentialId: \"dGVzdC1jcmVkZW50aWFs\",\n * publicKey: publicKeyBytes,\n * algorithm: -7,\n * counter: 0,\n * transports: [\"internal\"],\n * deviceType: \"multiDevice\",\n * backedUp: true,\n * name: \"MacBook Pro Touch ID\",\n * createdAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const passkeyInsert = mutation({\n args: {\n userId: v.id(\"User\"),\n credentialId: v.string(),\n publicKey: v.bytes(),\n algorithm: v.number(),\n counter: v.number(),\n transports: v.optional(v.array(v.string())),\n deviceType: v.string(),\n backedUp: v.boolean(),\n name: v.optional(v.string()),\n createdAt: v.number(),\n },\n returns: v.id(\"Passkey\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"Passkey\", args);\n },\n});\n\n/**\n * Look up a passkey by its credential ID.\n *\n * Queries the `Passkey` table using the `credential_id` unique index.\n * This is the primary lookup during a WebAuthn authentication ceremony:\n * the authenticator provides a credential ID, and this function retrieves\n * the corresponding public key and counter for signature verification.\n *\n * @param credentialId - Base64url-encoded credential identifier to search for.\n * @returns The matching `Passkey` document, or `null` if no passkey exists\n * with the given credential ID.\n *\n * @example\n * ```ts\n * const passkey = await ctx.runQuery(\n * components.auth.factors.passkeys.passkeyGetByCredentialId,\n * { credentialId: \"dGVzdC1jcmVkZW50aWFs\" },\n * );\n * if (passkey === null) {\n * throw new Error(\"Unknown credential\");\n * }\n * ```\n */\nexport const passkeyGetByCredentialId = query({\n args: { credentialId: v.string() },\n returns: v.union(vPasskeyDoc, v.null()),\n handler: async (ctx, { credentialId }) => {\n return await ctx.db\n .query(\"Passkey\")\n .withIndex(\"credential_id\", (q) => q.eq(\"credentialId\", credentialId))\n .unique();\n },\n});\n\n/**\n * List all passkeys registered to a user.\n *\n * Retrieves every `Passkey` document associated with the given user via\n * the `user_id` index. Useful for displaying a user's registered\n * authenticators in a settings page, or for building the\n * `allowCredentials` list during a WebAuthn authentication ceremony.\n *\n * @param userId - The `_id` of the `User` whose passkeys to retrieve.\n * @returns An array of `Passkey` documents. Returns an empty array if the\n * user has no registered passkeys.\n *\n * @example\n * ```ts\n * const passkeys = await ctx.runQuery(\n * components.auth.factors.passkeys.passkeyListByUserId,\n * { userId: user._id },\n * );\n * // Display each passkey's name and creation date\n * for (const pk of passkeys) {\n * console.log(pk.name, new Date(pk.createdAt));\n * }\n * ```\n */\nexport const passkeyListByUserId = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vPasskeyDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"Passkey\")\n .withIndex(\"user_id\", (q) => q.eq(\"userId\", userId))\n .collect();\n },\n});\n\n/**\n * Update a passkey's signature counter and last-used timestamp after\n * a successful authentication.\n *\n * After verifying a WebAuthn assertion, the relying party must persist\n * the new counter value reported by the authenticator. A counter that\n * does not increase may indicate a cloned credential.\n *\n * @param passkeyId - The `_id` of the `Passkey` document to update.\n * @param counter - The new signature counter value returned by the\n * authenticator in the assertion response.\n * @param lastUsedAt - Unix timestamp (in milliseconds) recording when\n * this passkey was most recently used to authenticate.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyUpdateCounter,\n * {\n * passkeyId: passkey._id,\n * counter: assertionResponse.counter,\n * lastUsedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const passkeyUpdateCounter = mutation({\n args: {\n passkeyId: v.id(\"Passkey\"),\n counter: v.number(),\n lastUsedAt: v.number(),\n },\n returns: v.null(),\n handler: async (ctx, { passkeyId, counter, lastUsedAt }) => {\n await ctx.db.patch(\"Passkey\", passkeyId, { counter, lastUsedAt });\n return null;\n },\n});\n\n/**\n * Update a passkey's metadata fields.\n *\n * Performs a partial patch on the `Passkey` document. Typically used to\n * rename a passkey (e.g. from `\"Security Key\"` to `\"YubiKey 5C\"`), but\n * can update any mutable fields via the `data` argument.\n *\n * @param passkeyId - The `_id` of the `Passkey` document to update.\n * @param data - An object containing the fields to patch. Commonly\n * includes `{ name: \"New Label\" }`, but accepts any valid passkey fields.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyUpdateMeta,\n * {\n * passkeyId: passkey._id,\n * data: { name: \"YubiKey 5C NFC\" },\n * },\n * );\n * ```\n */\nexport const passkeyUpdateMeta = mutation({\n args: { passkeyId: v.id(\"Passkey\"), data: v.any() },\n returns: v.null(),\n handler: async (ctx, { passkeyId, data }) => {\n await ctx.db.patch(\"Passkey\", passkeyId, data);\n return null;\n },\n});\n\n/**\n * Delete a passkey credential from the `Passkey` table.\n *\n * Permanently removes the passkey record. After deletion the credential\n * can no longer be used for authentication. Typically called from a\n * user's security settings when they want to unregister an authenticator.\n *\n * @param passkeyId - The `_id` of the `Passkey` document to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.passkeys.passkeyDelete,\n * { passkeyId: passkey._id },\n * );\n * ```\n */\nexport const passkeyDelete = mutation({\n args: { passkeyId: v.id(\"Passkey\") },\n returns: v.null(),\n handler: async (ctx, { passkeyId }) => {\n await ctx.db.delete(\"Passkey\", passkeyId);\n return null;\n },\n});\n\n// ============================================================================\n// TOTP Two-Factor Authentication\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,MAAa,gBAAgB,SAAS;CACpC,MAAM;EACJ,QAAQ,EAAE,GAAG,OAAO;EACpB,cAAc,EAAE,QAAQ;EACxB,WAAW,EAAE,OAAO;EACpB,WAAW,EAAE,QAAQ;EACrB,SAAS,EAAE,QAAQ;EACnB,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;EAC3C,YAAY,EAAE,QAAQ;EACtB,UAAU,EAAE,SAAS;EACrB,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC5B,WAAW,EAAE,QAAQ;EACtB;CACD,SAAS,EAAE,GAAG,UAAU;CACxB,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,WAAW,KAAK;;CAE9C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,2BAA2B,MAAM;CAC5C,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE;CAClC,SAAS,EAAE,MAAM,aAAa,EAAE,MAAM,CAAC;CACvC,SAAS,OAAO,KAAK,EAAE,mBAAmB;AACxC,SAAO,MAAM,IAAI,GACd,MAAM,UAAU,CAChB,UAAU,kBAAkB,MAAM,EAAE,GAAG,gBAAgB,aAAa,CAAC,CACrE,QAAQ;;CAEd,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,sBAAsB,MAAM;CACvC,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,OAAO,CAAC,CACnD,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BF,MAAa,uBAAuB,SAAS;CAC3C,MAAM;EACJ,WAAW,EAAE,GAAG,UAAU;EAC1B,SAAS,EAAE,QAAQ;EACnB,YAAY,EAAE,QAAQ;EACvB;CACD,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,WAAW,SAAS,iBAAiB;AAC1D,QAAM,IAAI,GAAG,MAAM,WAAW,WAAW;GAAE;GAAS;GAAY,CAAC;AACjE,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,oBAAoB,SAAS;CACxC,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;;;;;;;;;;;;;;;;;;;AAoBF,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":"totp.d.ts","names":[],"sources":["../../../../src/component/public/factors/totp.ts"],"mappings":";;;;;;;;;;;;;;;AAyCA;;;;;AAwCA;;;;;AAoCA;;;;;AAkCA;;;;;AAkCA;;;;;cAhJa,UAAA;;;;;AA6Mb;;;;;;;;;;;;;;;;;;;;cArKa,uBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAkCA,WAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCA,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAgCA,kBAAA;;;;;;;;;;;;;;;;;;;;;cA6BA,UAAA"}
1
+ {"version":3,"file":"totp.d.ts","names":[],"sources":["../../../../src/component/public/factors/totp.ts"],"mappings":";;;;;;;;;;;;;;;AA0CA;;;;;AAwCA;;;;;AAoCA;;;;;AAkCA;;;;;AAkCA;;;;;cAhJa,UAAA;;;;;AA6Mb;;;;;;;;;;;;;;;;;;;;cArKa,uBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAoCA,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAkCA,WAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCA,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;cAgCA,kBAAA;;;;;;;;;;;;;;;;;;;;;cA6BA,UAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"totp.js","names":[],"sources":["../../../../src/component/public/factors/totp.ts"],"sourcesContent":["import { v } from \"convex/values\";\nimport { mutation, query } from \"../../functions\";\nimport { vTotpFactorDoc } from \"../../model\";\n\n/**\n * Store a new TOTP (Time-based One-Time Password) enrollment for a user.\n *\n * Creates a `TotpFactor` record containing the shared secret and OTP\n * parameters. The enrollment starts in an unverified state until the\n * user confirms it by submitting a valid code generated from the secret.\n *\n * @param userId - The `_id` of the `User` enrolling in TOTP-based 2FA.\n * @param secret - The shared secret key as raw bytes, typically 20 bytes\n * of cryptographically random data.\n * @param digits - Number of digits in the generated OTP code (usually `6`).\n * @param period - Time step in seconds for code generation (usually `30`).\n * @param verified - Whether the enrollment has been verified. Set to\n * `false` during initial setup; set to `true` after the user submits\n * a valid code.\n * @param name - Optional human-readable label for the TOTP factor\n * (e.g. `\"Google Authenticator\"`).\n * @param createdAt - Unix timestamp (in milliseconds) when the enrollment\n * was created.\n * @returns The `_id` of the newly created `TotpFactor` document.\n *\n * @example\n * ```ts\n * const totpId = await ctx.runMutation(\n * components.auth.factors.totp.totpInsert,\n * {\n * userId: user._id,\n * secret: crypto.getRandomValues(new Uint8Array(20)),\n * digits: 6,\n * period: 30,\n * verified: false,\n * name: \"Authenticator App\",\n * createdAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const totpInsert = mutation({\n args: {\n userId: v.id(\"User\"),\n secret: v.bytes(),\n digits: v.number(),\n period: v.number(),\n verified: v.boolean(),\n name: v.optional(v.string()),\n createdAt: v.number(),\n },\n returns: v.id(\"TotpFactor\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"TotpFactor\", args);\n },\n});\n\n/**\n * Get a verified TOTP enrollment for a user.\n *\n * Queries the `TotpFactor` table using the `user_id_verified` compound\n * index to find the first enrollment that has been successfully verified.\n * This is the primary lookup during a TOTP authentication challenge --\n * only verified enrollments should be used to validate codes.\n *\n * @param userId - The `_id` of the `User` whose verified TOTP enrollment\n * to retrieve.\n * @returns The first verified `TotpFactor` document for the user, or\n * `null` if the user has no verified TOTP enrollment.\n *\n * @example\n * ```ts\n * const totp = await ctx.runQuery(\n * components.auth.factors.totp.totpGetVerifiedByUserId,\n * { userId: user._id },\n * );\n * if (totp === null) {\n * // User does not have TOTP 2FA enabled\n * }\n * ```\n */\nexport const totpGetVerifiedByUserId = query({\n args: { userId: v.id(\"User\") },\n returns: v.union(vTotpFactorDoc, v.null()),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"TotpFactor\")\n .withIndex(\"user_id_verified\", (q) =>\n q.eq(\"userId\", userId).eq(\"verified\", true),\n )\n .first();\n },\n});\n\n/**\n * List all TOTP enrollments for a user, both verified and unverified.\n *\n * Retrieves every `TotpFactor` document associated with the given user\n * via the `user_id` index. Useful for displaying enrolled authenticator\n * apps in a security settings page, including pending (unverified)\n * enrollments that the user has not yet confirmed.\n *\n * @param userId - The `_id` of the `User` whose TOTP enrollments to\n * retrieve.\n * @returns An array of `TotpFactor` documents. Returns an empty array if\n * the user has no TOTP enrollments.\n *\n * @example\n * ```ts\n * const factors = await ctx.runQuery(\n * components.auth.factors.totp.totpListByUserId,\n * { userId: user._id },\n * );\n * const verified = factors.filter((f) => f.verified);\n * const pending = factors.filter((f) => !f.verified);\n * ```\n */\nexport const totpListByUserId = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vTotpFactorDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"TotpFactor\")\n .withIndex(\"user_id\", (q) => q.eq(\"userId\", userId))\n .collect();\n },\n});\n\n/**\n * Get a single TOTP enrollment by its document ID.\n *\n * Performs a direct document lookup on the `TotpFactor` table. This is\n * used when you already have the enrollment's `_id` (e.g. from a\n * previous list query) and need to fetch its full details, including\n * the secret and verification status.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to retrieve.\n * @returns The `TotpFactor` document, or `null` if no enrollment exists\n * with the given ID.\n *\n * @example\n * ```ts\n * const totp = await ctx.runQuery(\n * components.auth.factors.totp.totpGetById,\n * { totpId: enrollmentId },\n * );\n * if (totp !== null && !totp.verified) {\n * // Enrollment is still pending confirmation\n * }\n * ```\n */\nexport const totpGetById = query({\n args: { totpId: v.id(\"TotpFactor\") },\n returns: v.union(vTotpFactorDoc, v.null()),\n handler: async (ctx, { totpId }) => {\n return await ctx.db.get(\"TotpFactor\", totpId);\n },\n});\n\n/**\n * Mark a TOTP enrollment as verified, completing the setup process.\n *\n * Called after the user successfully submits a valid TOTP code during\n * enrollment. This transitions the factor from a pending state to an\n * active, verified state, enabling it for future authentication\n * challenges.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to mark as\n * verified.\n * @param lastUsedAt - Unix timestamp (in milliseconds) recording when\n * the verification code was successfully validated.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // After validating the user's TOTP code during setup\n * await ctx.runMutation(\n * components.auth.factors.totp.totpMarkVerified,\n * {\n * totpId: enrollment._id,\n * lastUsedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const totpMarkVerified = mutation({\n args: { totpId: v.id(\"TotpFactor\"), lastUsedAt: v.number() },\n returns: v.null(),\n handler: async (ctx, { totpId, lastUsedAt }) => {\n await ctx.db.patch(\"TotpFactor\", totpId, { verified: true, lastUsedAt });\n return null;\n },\n});\n\n/**\n * Update a TOTP enrollment's last-used timestamp.\n *\n * Called after each successful TOTP code validation during sign-in.\n * Tracking the last-used time helps detect stale enrollments and can\n * be surfaced in security settings for user awareness.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to update.\n * @param lastUsedAt - Unix timestamp (in milliseconds) recording when\n * the TOTP code was most recently validated.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.totp.totpUpdateLastUsed,\n * {\n * totpId: totp._id,\n * lastUsedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const totpUpdateLastUsed = mutation({\n args: { totpId: v.id(\"TotpFactor\"), lastUsedAt: v.number() },\n returns: v.null(),\n handler: async (ctx, { totpId, lastUsedAt }) => {\n await ctx.db.patch(\"TotpFactor\", totpId, { lastUsedAt });\n return null;\n },\n});\n\n/**\n * Delete a TOTP enrollment from the `TotpFactor` table.\n *\n * Permanently removes the TOTP factor record, including its shared\n * secret. After deletion the user can no longer use this factor for\n * two-factor authentication. Typically called when a user disables\n * TOTP 2FA or wants to re-enroll with a new secret.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // User disables TOTP 2FA\n * await ctx.runMutation(\n * components.auth.factors.totp.totpDelete,\n * { totpId: totp._id },\n * );\n * ```\n */\nexport const totpDelete = mutation({\n args: { totpId: v.id(\"TotpFactor\") },\n returns: v.null(),\n handler: async (ctx, { totpId }) => {\n await ctx.db.delete(\"TotpFactor\", totpId);\n return null;\n },\n});\n\n// ============================================================================\n// Rate Limits\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAa,aAAa,SAAS;CACjC,MAAM;EACJ,QAAQ,EAAE,GAAG,OAAO;EACpB,QAAQ,EAAE,OAAO;EACjB,QAAQ,EAAE,QAAQ;EAClB,QAAQ,EAAE,QAAQ;EAClB,UAAU,EAAE,SAAS;EACrB,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC5B,WAAW,EAAE,QAAQ;EACtB;CACD,SAAS,EAAE,GAAG,aAAa;CAC3B,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,cAAc,KAAK;;CAEjD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,0BAA0B,MAAM;CAC3C,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,qBAAqB,MAC9B,EAAE,GAAG,UAAU,OAAO,CAAC,GAAG,YAAY,KAAK,CAC5C,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,mBAAmB,MAAM;CACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,eAAe;CAChC,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,YAAY,MAAM,EAAE,GAAG,UAAU,OAAO,CAAC,CACnD,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,cAAc,MAAM;CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE;CACpC,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GAAG,IAAI,cAAc,OAAO;;CAEhD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BF,MAAa,mBAAmB,SAAS;CACvC,MAAM;EAAE,QAAQ,EAAE,GAAG,aAAa;EAAE,YAAY,EAAE,QAAQ;EAAE;CAC5D,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,QAAQ,iBAAiB;AAC9C,QAAM,IAAI,GAAG,MAAM,cAAc,QAAQ;GAAE,UAAU;GAAM;GAAY,CAAC;AACxE,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,qBAAqB,SAAS;CACzC,MAAM;EAAE,QAAQ,EAAE,GAAG,aAAa;EAAE,YAAY,EAAE,QAAQ;EAAE;CAC5D,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,QAAQ,iBAAiB;AAC9C,QAAM,IAAI,GAAG,MAAM,cAAc,QAAQ,EAAE,YAAY,CAAC;AACxD,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,aAAa,SAAS;CACjC,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE;CACpC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,QAAM,IAAI,GAAG,OAAO,cAAc,OAAO;AACzC,SAAO;;CAEV,CAAC"}
1
+ {"version":3,"file":"totp.js","names":[],"sources":["../../../../src/component/public/factors/totp.ts"],"sourcesContent":["import { v } from \"convex/values\";\n\nimport { mutation, query } from \"../../functions\";\nimport { vTotpFactorDoc } from \"../../model\";\n\n/**\n * Store a new TOTP (Time-based One-Time Password) enrollment for a user.\n *\n * Creates a `TotpFactor` record containing the shared secret and OTP\n * parameters. The enrollment starts in an unverified state until the\n * user confirms it by submitting a valid code generated from the secret.\n *\n * @param userId - The `_id` of the `User` enrolling in TOTP-based 2FA.\n * @param secret - The shared secret key as raw bytes, typically 20 bytes\n * of cryptographically random data.\n * @param digits - Number of digits in the generated OTP code (usually `6`).\n * @param period - Time step in seconds for code generation (usually `30`).\n * @param verified - Whether the enrollment has been verified. Set to\n * `false` during initial setup; set to `true` after the user submits\n * a valid code.\n * @param name - Optional human-readable label for the TOTP factor\n * (e.g. `\"Google Authenticator\"`).\n * @param createdAt - Unix timestamp (in milliseconds) when the enrollment\n * was created.\n * @returns The `_id` of the newly created `TotpFactor` document.\n *\n * @example\n * ```ts\n * const totpId = await ctx.runMutation(\n * components.auth.factors.totp.totpInsert,\n * {\n * userId: user._id,\n * secret: crypto.getRandomValues(new Uint8Array(20)),\n * digits: 6,\n * period: 30,\n * verified: false,\n * name: \"Authenticator App\",\n * createdAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const totpInsert = mutation({\n args: {\n userId: v.id(\"User\"),\n secret: v.bytes(),\n digits: v.number(),\n period: v.number(),\n verified: v.boolean(),\n name: v.optional(v.string()),\n createdAt: v.number(),\n },\n returns: v.id(\"TotpFactor\"),\n handler: async (ctx, args) => {\n return await ctx.db.insert(\"TotpFactor\", args);\n },\n});\n\n/**\n * Get a verified TOTP enrollment for a user.\n *\n * Queries the `TotpFactor` table using the `user_id_verified` compound\n * index to find the first enrollment that has been successfully verified.\n * This is the primary lookup during a TOTP authentication challenge --\n * only verified enrollments should be used to validate codes.\n *\n * @param userId - The `_id` of the `User` whose verified TOTP enrollment\n * to retrieve.\n * @returns The first verified `TotpFactor` document for the user, or\n * `null` if the user has no verified TOTP enrollment.\n *\n * @example\n * ```ts\n * const totp = await ctx.runQuery(\n * components.auth.factors.totp.totpGetVerifiedByUserId,\n * { userId: user._id },\n * );\n * if (totp === null) {\n * // User does not have TOTP 2FA enabled\n * }\n * ```\n */\nexport const totpGetVerifiedByUserId = query({\n args: { userId: v.id(\"User\") },\n returns: v.union(vTotpFactorDoc, v.null()),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"TotpFactor\")\n .withIndex(\"user_id_verified\", (q) =>\n q.eq(\"userId\", userId).eq(\"verified\", true),\n )\n .first();\n },\n});\n\n/**\n * List all TOTP enrollments for a user, both verified and unverified.\n *\n * Retrieves every `TotpFactor` document associated with the given user\n * via the `user_id` index. Useful for displaying enrolled authenticator\n * apps in a security settings page, including pending (unverified)\n * enrollments that the user has not yet confirmed.\n *\n * @param userId - The `_id` of the `User` whose TOTP enrollments to\n * retrieve.\n * @returns An array of `TotpFactor` documents. Returns an empty array if\n * the user has no TOTP enrollments.\n *\n * @example\n * ```ts\n * const factors = await ctx.runQuery(\n * components.auth.factors.totp.totpListByUserId,\n * { userId: user._id },\n * );\n * const verified = factors.filter((f) => f.verified);\n * const pending = factors.filter((f) => !f.verified);\n * ```\n */\nexport const totpListByUserId = query({\n args: { userId: v.id(\"User\") },\n returns: v.array(vTotpFactorDoc),\n handler: async (ctx, { userId }) => {\n return await ctx.db\n .query(\"TotpFactor\")\n .withIndex(\"user_id\", (q) => q.eq(\"userId\", userId))\n .collect();\n },\n});\n\n/**\n * Get a single TOTP enrollment by its document ID.\n *\n * Performs a direct document lookup on the `TotpFactor` table. This is\n * used when you already have the enrollment's `_id` (e.g. from a\n * previous list query) and need to fetch its full details, including\n * the secret and verification status.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to retrieve.\n * @returns The `TotpFactor` document, or `null` if no enrollment exists\n * with the given ID.\n *\n * @example\n * ```ts\n * const totp = await ctx.runQuery(\n * components.auth.factors.totp.totpGetById,\n * { totpId: enrollmentId },\n * );\n * if (totp !== null && !totp.verified) {\n * // Enrollment is still pending confirmation\n * }\n * ```\n */\nexport const totpGetById = query({\n args: { totpId: v.id(\"TotpFactor\") },\n returns: v.union(vTotpFactorDoc, v.null()),\n handler: async (ctx, { totpId }) => {\n return await ctx.db.get(\"TotpFactor\", totpId);\n },\n});\n\n/**\n * Mark a TOTP enrollment as verified, completing the setup process.\n *\n * Called after the user successfully submits a valid TOTP code during\n * enrollment. This transitions the factor from a pending state to an\n * active, verified state, enabling it for future authentication\n * challenges.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to mark as\n * verified.\n * @param lastUsedAt - Unix timestamp (in milliseconds) recording when\n * the verification code was successfully validated.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // After validating the user's TOTP code during setup\n * await ctx.runMutation(\n * components.auth.factors.totp.totpMarkVerified,\n * {\n * totpId: enrollment._id,\n * lastUsedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const totpMarkVerified = mutation({\n args: { totpId: v.id(\"TotpFactor\"), lastUsedAt: v.number() },\n returns: v.null(),\n handler: async (ctx, { totpId, lastUsedAt }) => {\n await ctx.db.patch(\"TotpFactor\", totpId, { verified: true, lastUsedAt });\n return null;\n },\n});\n\n/**\n * Update a TOTP enrollment's last-used timestamp.\n *\n * Called after each successful TOTP code validation during sign-in.\n * Tracking the last-used time helps detect stale enrollments and can\n * be surfaced in security settings for user awareness.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to update.\n * @param lastUsedAt - Unix timestamp (in milliseconds) recording when\n * the TOTP code was most recently validated.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * await ctx.runMutation(\n * components.auth.factors.totp.totpUpdateLastUsed,\n * {\n * totpId: totp._id,\n * lastUsedAt: Date.now(),\n * },\n * );\n * ```\n */\nexport const totpUpdateLastUsed = mutation({\n args: { totpId: v.id(\"TotpFactor\"), lastUsedAt: v.number() },\n returns: v.null(),\n handler: async (ctx, { totpId, lastUsedAt }) => {\n await ctx.db.patch(\"TotpFactor\", totpId, { lastUsedAt });\n return null;\n },\n});\n\n/**\n * Delete a TOTP enrollment from the `TotpFactor` table.\n *\n * Permanently removes the TOTP factor record, including its shared\n * secret. After deletion the user can no longer use this factor for\n * two-factor authentication. Typically called when a user disables\n * TOTP 2FA or wants to re-enroll with a new secret.\n *\n * @param totpId - The `_id` of the `TotpFactor` document to delete.\n * @returns `null` on success.\n *\n * @example\n * ```ts\n * // User disables TOTP 2FA\n * await ctx.runMutation(\n * components.auth.factors.totp.totpDelete,\n * { totpId: totp._id },\n * );\n * ```\n */\nexport const totpDelete = mutation({\n args: { totpId: v.id(\"TotpFactor\") },\n returns: v.null(),\n handler: async (ctx, { totpId }) => {\n await ctx.db.delete(\"TotpFactor\", totpId);\n return null;\n },\n});\n\n// ============================================================================\n// Rate Limits\n// ============================================================================\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,aAAa,SAAS;CACjC,MAAM;EACJ,QAAQ,EAAE,GAAG,OAAO;EACpB,QAAQ,EAAE,OAAO;EACjB,QAAQ,EAAE,QAAQ;EAClB,QAAQ,EAAE,QAAQ;EAClB,UAAU,EAAE,SAAS;EACrB,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC5B,WAAW,EAAE,QAAQ;EACtB;CACD,SAAS,EAAE,GAAG,aAAa;CAC3B,SAAS,OAAO,KAAK,SAAS;AAC5B,SAAO,MAAM,IAAI,GAAG,OAAO,cAAc,KAAK;;CAEjD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;AA0BF,MAAa,0BAA0B,MAAM;CAC3C,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,qBAAqB,MAC9B,EAAE,GAAG,UAAU,OAAO,CAAC,GAAG,YAAY,KAAK,CAC5C,CACA,OAAO;;CAEb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,mBAAmB,MAAM;CACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE;CAC9B,SAAS,EAAE,MAAM,eAAe;CAChC,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GACd,MAAM,aAAa,CACnB,UAAU,YAAY,MAAM,EAAE,GAAG,UAAU,OAAO,CAAC,CACnD,SAAS;;CAEf,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,cAAc,MAAM;CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE;CACpC,SAAS,EAAE,MAAM,gBAAgB,EAAE,MAAM,CAAC;CAC1C,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,SAAO,MAAM,IAAI,GAAG,IAAI,cAAc,OAAO;;CAEhD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BF,MAAa,mBAAmB,SAAS;CACvC,MAAM;EAAE,QAAQ,EAAE,GAAG,aAAa;EAAE,YAAY,EAAE,QAAQ;EAAE;CAC5D,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,QAAQ,iBAAiB;AAC9C,QAAM,IAAI,GAAG,MAAM,cAAc,QAAQ;GAAE,UAAU;GAAM;GAAY,CAAC;AACxE,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAyBF,MAAa,qBAAqB,SAAS;CACzC,MAAM;EAAE,QAAQ,EAAE,GAAG,aAAa;EAAE,YAAY,EAAE,QAAQ;EAAE;CAC5D,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,QAAQ,iBAAiB;AAC9C,QAAM,IAAI,GAAG,MAAM,cAAc,QAAQ,EAAE,YAAY,CAAC;AACxD,SAAO;;CAEV,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBF,MAAa,aAAa,SAAS;CACjC,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE;CACpC,SAAS,EAAE,MAAM;CACjB,SAAS,OAAO,KAAK,EAAE,aAAa;AAClC,QAAM,IAAI,GAAG,OAAO,cAAc,OAAO;AACzC,SAAO;;CAEV,CAAC"}
@@ -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":"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"}
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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"}