@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
@@ -10,7 +10,7 @@
10
10
  * Uses `@oslojs/webauthn` for attestation/assertion parsing and
11
11
  * `@oslojs/crypto` for signature verification.
12
12
  *
13
- * All functions return `Fx<A, AuthError>` composed via `Fx.chain` pipelines.
13
+ * All functions return `Fx<A, ConvexError<any>>` composed via `Fx.chain` pipelines.
14
14
  *
15
15
  * @module
16
16
  */
@@ -43,11 +43,11 @@ import {
43
43
  COSEKeyType,
44
44
  } from "@oslojs/webauthn";
45
45
  import type { Fx as FxType } from "@robelest/fx";
46
-
47
- import { authDb } from "./db";
48
46
  import { Fx } from "@robelest/fx";
47
+ import { Cv } from "@robelest/fx/convex";
48
+ import type { ConvexError } from "convex/values";
49
49
 
50
- import { AuthError } from "./authError";
50
+ import { authDb } from "./db";
51
51
  import { userIdFromIdentitySubject } from "./identity";
52
52
  import { callSignIn, callVerifier } from "./mutations/index";
53
53
  import { callVerifierSignature } from "./mutations/signature";
@@ -87,12 +87,12 @@ interface RpOptions {
87
87
  /**
88
88
  * Resolve passkey relying party options from provider config and environment.
89
89
  *
90
- * Returns `Fx<RpOptions, AuthError>` — fails if neither SITE_URL nor rpId
90
+ * Returns `Fx<RpOptions, ConvexError<any>>` — fails if neither SITE_URL nor rpId
91
91
  * is configured.
92
92
  */
93
93
  const resolveRpOptionsFx = (
94
94
  provider: PasskeyProviderConfig,
95
- ): FxType<RpOptions, AuthError> => {
95
+ ): FxType<RpOptions, ConvexError<any>> => {
96
96
  const siteUrl = process.env.SITE_URL;
97
97
  const hasSiteUrl = siteUrl !== undefined && siteUrl !== "";
98
98
  const hasRpId = provider.options.rpId !== undefined;
@@ -100,14 +100,13 @@ const resolveRpOptionsFx = (
100
100
  return Fx.succeed({ siteUrl, hasSiteUrl, hasRpId }).pipe(
101
101
  Fx.chain(({ siteUrl, hasSiteUrl, hasRpId }) =>
102
102
  !hasSiteUrl && !hasRpId
103
- ? Fx.fail(
104
- new AuthError(
105
- "PASSKEY_MISSING_CONFIG",
103
+ ? Cv.fail({
104
+ code: "PASSKEY_MISSING_CONFIG",
105
+ message:
106
106
  "Passkey provider requires SITE_URL env var (your frontend URL) " +
107
- "or explicit rpId / origin in the provider config. " +
108
- "CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.",
109
- ),
110
- )
107
+ "or explicit rpId / origin in the provider config. " +
108
+ "CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.",
109
+ })
111
110
  : Fx.succeed(siteUrl),
112
111
  ),
113
112
  Fx.map((siteUrl) => {
@@ -132,7 +131,7 @@ const resolveRpOptionsFx = (
132
131
  };
133
132
 
134
133
  // ============================================================================
135
- // Composable validators — small functions (A) => Fx<B, AuthError>
134
+ // Composable validators — small functions (A) => Fx<B, ConvexError<any>>
136
135
  // ============================================================================
137
136
 
138
137
  /** Verify client data type matches expected WebAuthn ceremony type. */
@@ -141,29 +140,27 @@ const verifyClientDataType =
141
140
  expectedType: ClientDataType,
142
141
  label: string,
143
142
  ) =>
144
- (clientData: T): FxType<T, AuthError> =>
143
+ (clientData: T): FxType<T, ConvexError<any>> =>
145
144
  clientData.type === expectedType
146
145
  ? Fx.succeed(clientData)
147
- : Fx.fail(
148
- new AuthError(
149
- "PASSKEY_INVALID_CLIENT_DATA",
150
- `Invalid client data type: expected ${label}`,
151
- ),
152
- );
146
+ : Cv.fail({
147
+ code: "PASSKEY_INVALID_CLIENT_DATA",
148
+ message: `Invalid client data type: expected ${label}`,
149
+ });
153
150
 
154
151
  /** Verify origin is in the allowed list. */
155
152
  const verifyOrigin =
156
153
  (rp: RpOptions) =>
157
- <T extends { origin: string }>(clientData: T): FxType<T, AuthError> => {
154
+ <T extends { origin: string }>(
155
+ clientData: T,
156
+ ): FxType<T, ConvexError<any>> => {
158
157
  const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
159
158
  return allowed.includes(clientData.origin)
160
159
  ? Fx.succeed(clientData)
161
- : Fx.fail(
162
- new AuthError(
163
- "PASSKEY_INVALID_ORIGIN",
164
- `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`,
165
- ),
166
- );
160
+ : Cv.fail({
161
+ code: "PASSKEY_INVALID_ORIGIN",
162
+ message: `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`,
163
+ });
167
164
  };
168
165
 
169
166
  /** Verify the challenge hash matches the stored verifier, then delete verifier. */
@@ -171,23 +168,34 @@ const verifyAndConsumeChallenge =
171
168
  (ctx: EnrichedActionCtx, verifierValue: string) =>
172
169
  <T extends { challenge: Uint8Array }>(
173
170
  clientData: T,
174
- ): FxType<T, AuthError> => {
171
+ ): FxType<T, ConvexError<any>> => {
175
172
  const challengeHash = encodeBase64urlNoPadding(
176
173
  new Uint8Array(sha256(clientData.challenge)),
177
174
  );
178
175
  return Fx.from({
179
176
  ok: () => queryVerifierById(ctx, verifierValue),
180
- err: () => new AuthError("PASSKEY_INVALID_CHALLENGE"),
177
+ err: () =>
178
+ Cv.error({
179
+ code: "PASSKEY_INVALID_CHALLENGE",
180
+ message: "Invalid or expired passkey challenge.",
181
+ }),
181
182
  }).pipe(
182
183
  Fx.chain((doc) =>
183
184
  !doc || doc.signature !== challengeHash
184
- ? Fx.fail(new AuthError("PASSKEY_INVALID_CHALLENGE"))
185
+ ? Cv.fail({
186
+ code: "PASSKEY_INVALID_CHALLENGE",
187
+ message: "Invalid or expired passkey challenge.",
188
+ })
185
189
  : Fx.succeed(doc),
186
190
  ),
187
191
  Fx.chain(() =>
188
192
  Fx.from({
189
193
  ok: () => mutateVerifierDelete(ctx, verifierValue),
190
- err: () => new AuthError("PASSKEY_INVALID_CHALLENGE"),
194
+ err: () =>
195
+ Cv.error({
196
+ code: "PASSKEY_INVALID_CHALLENGE",
197
+ message: "Invalid or expired passkey challenge.",
198
+ }),
191
199
  }),
192
200
  ),
193
201
  Fx.map(() => clientData),
@@ -199,21 +207,30 @@ const verifyRpId =
199
207
  (rpId: string) =>
200
208
  <T extends { verifyRelyingPartyIdHash: (id: string) => boolean }>(
201
209
  authData: T,
202
- ): FxType<T, AuthError> =>
210
+ ): FxType<T, ConvexError<any>> =>
203
211
  authData.verifyRelyingPartyIdHash(rpId)
204
212
  ? Fx.succeed(authData)
205
- : Fx.fail(new AuthError("PASSKEY_RP_MISMATCH"));
213
+ : Cv.fail({
214
+ code: "PASSKEY_RP_MISMATCH",
215
+ message: "Relying party ID mismatch.",
216
+ });
206
217
 
207
218
  /** Verify user presence and (optionally) user verification flags. */
208
219
  const verifyUserFlags =
209
220
  (rp: RpOptions) =>
210
221
  <T extends { userPresent: boolean; userVerified: boolean }>(
211
222
  authData: T,
212
- ): FxType<T, AuthError> =>
223
+ ): FxType<T, ConvexError<any>> =>
213
224
  !authData.userPresent
214
- ? Fx.fail(new AuthError("PASSKEY_USER_PRESENCE"))
225
+ ? Cv.fail({
226
+ code: "PASSKEY_USER_PRESENCE",
227
+ message: "User presence flag not set.",
228
+ })
215
229
  : rp.userVerification === "required" && !authData.userVerified
216
- ? Fx.fail(new AuthError("PASSKEY_USER_VERIFICATION"))
230
+ ? Cv.fail({
231
+ code: "PASSKEY_USER_VERIFICATION",
232
+ message: "User verification required but not performed.",
233
+ })
217
234
  : Fx.succeed(authData);
218
235
 
219
236
  // ============================================================================
@@ -255,24 +272,26 @@ type PasskeyDispatch =
255
272
 
256
273
  const resolvePasskeyDispatchFx = (
257
274
  params: Record<string, unknown>,
258
- ): FxType<PasskeyDispatch, AuthError> => {
275
+ ): FxType<PasskeyDispatch, ConvexError<any>> => {
259
276
  const flow = params.flow;
260
277
  return typeof flow === "string" && PASSKEY_FLOWS.includes(flow as never)
261
278
  ? Fx.succeed({ flow: flow as (typeof PASSKEY_FLOWS)[number] })
262
- : Fx.fail(
263
- new AuthError(
264
- "PASSKEY_MISSING_FLOW",
279
+ : Cv.fail({
280
+ code: "PASSKEY_MISSING_FLOW",
281
+ message:
265
282
  "Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify",
266
- ),
267
- );
283
+ });
268
284
  };
269
285
 
270
286
  const requirePasskeyVerifierFx = (
271
287
  verifier: string | undefined,
272
- ): FxType<string, AuthError> =>
288
+ ): FxType<string, ConvexError<any>> =>
273
289
  verifier != null
274
290
  ? Fx.succeed(verifier)
275
- : Fx.fail(new AuthError("PASSKEY_MISSING_VERIFIER"));
291
+ : Cv.fail({
292
+ code: "PASSKEY_MISSING_VERIFIER",
293
+ message: "Missing verifier for passkey operation.",
294
+ });
276
295
 
277
296
  /**
278
297
  * Main passkey handler dispatched from signIn.ts.
@@ -286,498 +305,529 @@ export function handlePasskeyFx(
286
305
  params?: Record<string, any>;
287
306
  verifier?: string;
288
307
  },
289
- ): FxType<PasskeyResult, AuthError> {
308
+ ): FxType<PasskeyResult, ConvexError<any>> {
290
309
  const params = (args.params ?? {}) as Record<string, any>;
291
310
 
292
311
  return resolvePasskeyDispatchFx(params).pipe(
293
312
  Fx.chain((dispatch) => {
294
- const flowFx: FxType<PasskeyResult, AuthError> = Fx.match(dispatch).on(
295
- "flow",
296
- {
297
- registerOptions: (_) =>
298
- Fx.zip(
299
- Fx.from({
300
- ok: () => ctx.auth.getUserIdentity(),
301
- err: () => new AuthError("PASSKEY_AUTH_REQUIRED"),
302
- }).pipe(
303
- Fx.chain((id) =>
304
- id === null
305
- ? Fx.fail(new AuthError("PASSKEY_AUTH_REQUIRED"))
306
- : Fx.succeed(userIdFromIdentitySubject(id.subject)),
307
- ),
313
+ const flowFx: FxType<PasskeyResult, ConvexError<any>> = Fx.match(
314
+ dispatch,
315
+ ).on("flow", {
316
+ registerOptions: (_) =>
317
+ Fx.zip(
318
+ Fx.from({
319
+ ok: () => ctx.auth.getUserIdentity(),
320
+ err: () =>
321
+ Cv.error({
322
+ code: "PASSKEY_AUTH_REQUIRED",
323
+ message: "Sign in first, then add a passkey to your account.",
324
+ }),
325
+ }).pipe(
326
+ Fx.chain((id) =>
327
+ id === null
328
+ ? Cv.fail({
329
+ code: "PASSKEY_AUTH_REQUIRED",
330
+ message:
331
+ "Sign in first, then add a passkey to your account.",
332
+ })
333
+ : Fx.succeed(userIdFromIdentitySubject(id.subject)),
308
334
  ),
309
- resolveRpOptionsFx(provider),
310
- ).pipe(
311
- Fx.chain(([userId, rp]) => {
312
- const challenge = new Uint8Array(32);
313
- crypto.getRandomValues(challenge);
314
- const challengeHash = encodeBase64urlNoPadding(
315
- new Uint8Array(sha256(challenge)),
316
- );
317
-
318
- return Fx.from({
319
- ok: async () => {
320
- const verifier = await callVerifier(ctx);
321
- await callVerifierSignature(ctx, {
322
- verifier,
323
- signature: challengeHash,
324
- });
325
-
326
- const user = await queryUserById(ctx, userId);
327
- const userName = params.userName ?? user?.email ?? "user";
328
- const userDisplayName =
329
- params.userDisplayName ?? user?.name ?? userName;
330
-
331
- const existing = await queryPasskeysByUserId(ctx, userId);
332
- const excludeCredentials = existing.map((pk) => ({
333
- id: pk.credentialId,
334
- transports: pk.transports,
335
- }));
336
-
337
- const userHandle = encodeBase64urlNoPadding(
338
- new TextEncoder().encode(userId),
339
- );
340
-
341
- const options = {
342
- rp: { name: rp.rpName, id: rp.rpId },
343
- user: {
344
- id: userHandle,
345
- name: userName,
346
- displayName: userDisplayName,
347
- },
348
- challenge: encodeBase64urlNoPadding(challenge),
349
- pubKeyCredParams: rp.algorithms.map((alg) => ({
350
- type: "public-key" as const,
351
- alg,
352
- })),
353
- timeout: rp.challengeExpirationMs,
354
- attestation: rp.attestation,
355
- authenticatorSelection: {
356
- residentKey: rp.residentKey,
357
- requireResidentKey: rp.residentKey === "required",
358
- userVerification: rp.userVerification,
359
- ...(rp.authenticatorAttachment
360
- ? {
361
- authenticatorAttachment:
362
- rp.authenticatorAttachment,
363
- }
364
- : {}),
365
- },
366
- excludeCredentials,
367
- };
368
-
369
- return {
370
- kind: "passkeyOptions" as const,
371
- options,
372
- verifier,
373
- };
374
- },
375
- err: () => new AuthError("INTERNAL_ERROR"),
376
- });
377
- }),
378
335
  ),
379
- registerVerify: (_) =>
380
- Fx.zip(
381
- Fx.from({
382
- ok: () => ctx.auth.getUserIdentity(),
383
- err: () => new AuthError("PASSKEY_AUTH_REQUIRED"),
384
- }).pipe(
385
- Fx.chain((id) =>
386
- id === null
387
- ? Fx.fail(new AuthError("PASSKEY_AUTH_REQUIRED"))
388
- : Fx.succeed(userIdFromIdentitySubject(id.subject)),
389
- ),
336
+ resolveRpOptionsFx(provider),
337
+ ).pipe(
338
+ Fx.chain(([userId, rp]) => {
339
+ const challenge = new Uint8Array(32);
340
+ crypto.getRandomValues(challenge);
341
+ const challengeHash = encodeBase64urlNoPadding(
342
+ new Uint8Array(sha256(challenge)),
343
+ );
344
+
345
+ return Fx.from({
346
+ ok: async () => {
347
+ const verifier = await callVerifier(ctx);
348
+ await callVerifierSignature(ctx, {
349
+ verifier,
350
+ signature: challengeHash,
351
+ });
352
+
353
+ const user = await queryUserById(ctx, userId);
354
+ const userName = params.userName ?? user?.email ?? "user";
355
+ const userDisplayName =
356
+ params.userDisplayName ?? user?.name ?? userName;
357
+
358
+ const existing = await queryPasskeysByUserId(ctx, userId);
359
+ const excludeCredentials = existing.map((pk) => ({
360
+ id: pk.credentialId,
361
+ transports: pk.transports,
362
+ }));
363
+
364
+ const userHandle = encodeBase64urlNoPadding(
365
+ new TextEncoder().encode(userId),
366
+ );
367
+
368
+ const options = {
369
+ rp: { name: rp.rpName, id: rp.rpId },
370
+ user: {
371
+ id: userHandle,
372
+ name: userName,
373
+ displayName: userDisplayName,
374
+ },
375
+ challenge: encodeBase64urlNoPadding(challenge),
376
+ pubKeyCredParams: rp.algorithms.map((alg) => ({
377
+ type: "public-key" as const,
378
+ alg,
379
+ })),
380
+ timeout: rp.challengeExpirationMs,
381
+ attestation: rp.attestation,
382
+ authenticatorSelection: {
383
+ residentKey: rp.residentKey,
384
+ requireResidentKey: rp.residentKey === "required",
385
+ userVerification: rp.userVerification,
386
+ ...(rp.authenticatorAttachment
387
+ ? {
388
+ authenticatorAttachment: rp.authenticatorAttachment,
389
+ }
390
+ : {}),
391
+ },
392
+ excludeCredentials,
393
+ };
394
+
395
+ return {
396
+ kind: "passkeyOptions" as const,
397
+ options,
398
+ verifier,
399
+ };
400
+ },
401
+ err: () =>
402
+ Cv.error({
403
+ code: "INTERNAL_ERROR",
404
+ message: "An unexpected error occurred.",
405
+ }),
406
+ });
407
+ }),
408
+ ),
409
+ registerVerify: (_) =>
410
+ Fx.zip(
411
+ Fx.from({
412
+ ok: () => ctx.auth.getUserIdentity(),
413
+ err: () =>
414
+ Cv.error({
415
+ code: "PASSKEY_AUTH_REQUIRED",
416
+ message: "Sign in first, then add a passkey to your account.",
417
+ }),
418
+ }).pipe(
419
+ Fx.chain((id) =>
420
+ id === null
421
+ ? Cv.fail({
422
+ code: "PASSKEY_AUTH_REQUIRED",
423
+ message:
424
+ "Sign in first, then add a passkey to your account.",
425
+ })
426
+ : Fx.succeed(userIdFromIdentitySubject(id.subject)),
390
427
  ),
391
- resolveRpOptionsFx(provider),
392
- ).pipe(
393
- Fx.chain(([userId, rp]) =>
394
- requirePasskeyVerifierFx(args.verifier).pipe(
395
- Fx.chain((verifier) => {
396
- const clientDataJSON = decodeBase64urlIgnorePadding(
397
- params.clientDataJSON,
398
- );
399
- const clientData = parseClientDataJSON(clientDataJSON);
400
-
401
- const verifiedClientDataFx = Fx.succeed(clientData).pipe(
402
- Fx.chain(
403
- verifyClientDataType(
404
- ClientDataType.Create,
405
- "webauthn.create",
406
- ),
428
+ ),
429
+ resolveRpOptionsFx(provider),
430
+ ).pipe(
431
+ Fx.chain(([userId, rp]) =>
432
+ requirePasskeyVerifierFx(args.verifier).pipe(
433
+ Fx.chain((verifier) => {
434
+ const clientDataJSON = decodeBase64urlIgnorePadding(
435
+ params.clientDataJSON,
436
+ );
437
+ const clientData = parseClientDataJSON(clientDataJSON);
438
+
439
+ const verifiedClientDataFx = Fx.succeed(clientData).pipe(
440
+ Fx.chain(
441
+ verifyClientDataType(
442
+ ClientDataType.Create,
443
+ "webauthn.create",
407
444
  ),
408
- Fx.chain(verifyOrigin(rp)),
409
- Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),
410
- Fx.map(() => {
411
- const attestationObjectBytes =
412
- decodeBase64urlIgnorePadding(
413
- params.attestationObject,
414
- );
415
- const attestation = parseAttestationObject(
416
- attestationObjectBytes,
417
- );
418
- return attestation.authenticatorData;
419
- }),
420
- );
421
-
422
- return verifiedClientDataFx.pipe(
423
- Fx.chain(verifyRpId(rp.rpId)),
424
- Fx.chain(verifyUserFlags(rp)),
425
- Fx.chain((authData) => {
426
- if (authData.credential == null) {
427
- return Fx.fail(
428
- new AuthError("PASSKEY_NO_CREDENTIAL"),
429
- );
430
- }
431
- return Fx.succeed({
432
- authData,
433
- credential: authData.credential,
445
+ ),
446
+ Fx.chain(verifyOrigin(rp)),
447
+ Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),
448
+ Fx.map(() => {
449
+ const attestationObjectBytes =
450
+ decodeBase64urlIgnorePadding(params.attestationObject);
451
+ const attestation = parseAttestationObject(
452
+ attestationObjectBytes,
453
+ );
454
+ return attestation.authenticatorData;
455
+ }),
456
+ );
457
+
458
+ return verifiedClientDataFx.pipe(
459
+ Fx.chain(verifyRpId(rp.rpId)),
460
+ Fx.chain(verifyUserFlags(rp)),
461
+ Fx.chain((authData) => {
462
+ if (authData.credential == null) {
463
+ return Cv.fail({
464
+ code: "PASSKEY_NO_CREDENTIAL",
465
+ message: "No credential in attestation.",
434
466
  });
435
- }),
436
- Fx.chain(({ authData, credential }) => {
437
- const credentialId = encodeBase64urlNoPadding(
438
- credential.id,
439
- );
440
- const publicKey = credential.publicKey;
441
-
442
- let algorithm: number;
443
- if (publicKey.isAlgorithmDefined()) {
444
- algorithm = publicKey.algorithm();
445
- } else {
446
- const keyType = publicKey.type();
447
- algorithm =
448
- keyType === COSEKeyType.EC2
449
- ? coseAlgorithmES256
450
- : keyType === COSEKeyType.RSA
451
- ? coseAlgorithmRS256
452
- : coseAlgorithmES256;
453
- }
454
-
455
- const handlers: Record<
456
- number,
457
- (() => FxType<Uint8Array, AuthError>) | undefined
458
- > = {
459
- [coseAlgorithmES256]: () => {
460
- const ec2 = publicKey.ec2();
461
- const xBytes = new Uint8Array(32);
462
- let vx = ec2.x;
463
- for (let i = 31; i >= 0; i--) {
464
- xBytes[i] = Number(vx & 0xffn);
465
- vx >>= 8n;
466
- }
467
- const yBytes = new Uint8Array(32);
468
- let vy = ec2.y;
469
- for (let i = 31; i >= 0; i--) {
470
- yBytes[i] = Number(vy & 0xffn);
471
- vy >>= 8n;
472
- }
473
- const bytes = new Uint8Array(65);
474
- bytes[0] = 0x04;
475
- bytes.set(xBytes, 1);
476
- bytes.set(yBytes, 33);
477
- return Fx.succeed(bytes);
478
- },
479
- [coseAlgorithmRS256]: () => {
480
- const rsa = publicKey.rsa();
481
- const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);
482
- return Fx.succeed(rsaPubKey.encodePKCS1());
483
- },
484
- };
485
-
486
- const handler = handlers[algorithm];
487
- return (
488
- handler
489
- ? handler()
490
- : Fx.fail(
491
- new AuthError(
492
- "PASSKEY_UNSUPPORTED_ALGORITHM",
493
- `Unsupported algorithm: ${algorithm}`,
467
+ }
468
+ return Fx.succeed({
469
+ authData,
470
+ credential: authData.credential,
471
+ });
472
+ }),
473
+ Fx.chain(({ authData, credential }) => {
474
+ const credentialId = encodeBase64urlNoPadding(
475
+ credential.id,
476
+ );
477
+ const publicKey = credential.publicKey;
478
+
479
+ let algorithm: number;
480
+ if (publicKey.isAlgorithmDefined()) {
481
+ algorithm = publicKey.algorithm();
482
+ } else {
483
+ const keyType = publicKey.type();
484
+ algorithm =
485
+ keyType === COSEKeyType.EC2
486
+ ? coseAlgorithmES256
487
+ : keyType === COSEKeyType.RSA
488
+ ? coseAlgorithmRS256
489
+ : coseAlgorithmES256;
490
+ }
491
+
492
+ const handlers: Record<
493
+ number,
494
+ (() => FxType<Uint8Array, ConvexError<any>>) | undefined
495
+ > = {
496
+ [coseAlgorithmES256]: () => {
497
+ const ec2 = publicKey.ec2();
498
+ const xBytes = new Uint8Array(32);
499
+ let vx = ec2.x;
500
+ for (let i = 31; i >= 0; i--) {
501
+ xBytes[i] = Number(vx & 0xffn);
502
+ vx >>= 8n;
503
+ }
504
+ const yBytes = new Uint8Array(32);
505
+ let vy = ec2.y;
506
+ for (let i = 31; i >= 0; i--) {
507
+ yBytes[i] = Number(vy & 0xffn);
508
+ vy >>= 8n;
509
+ }
510
+ const bytes = new Uint8Array(65);
511
+ bytes[0] = 0x04;
512
+ bytes.set(xBytes, 1);
513
+ bytes.set(yBytes, 33);
514
+ return Fx.succeed(bytes);
515
+ },
516
+ [coseAlgorithmRS256]: () => {
517
+ const rsa = publicKey.rsa();
518
+ const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);
519
+ return Fx.succeed(rsaPubKey.encodePKCS1());
520
+ },
521
+ };
522
+
523
+ const handler = handlers[algorithm];
524
+ return (
525
+ handler
526
+ ? handler()
527
+ : Cv.fail({
528
+ code: "PASSKEY_UNSUPPORTED_ALGORITHM",
529
+ message: `Unsupported algorithm: ${algorithm}`,
530
+ })
531
+ ).pipe(
532
+ Fx.chain((publicKeyBytes) =>
533
+ Fx.from({
534
+ ok: async () => {
535
+ const deviceType =
536
+ params.deviceType ?? "single-device";
537
+ const backedUp = params.backedUp ?? false;
538
+
539
+ const db = authDb(ctx, ctx.auth.config);
540
+ await db.accounts.create({
541
+ userId,
542
+ provider: provider.id,
543
+ providerAccountId: credentialId,
544
+ });
545
+
546
+ await mutatePasskeyInsert(ctx, {
547
+ userId,
548
+ credentialId,
549
+ publicKey: publicKeyBytes.buffer.slice(
550
+ publicKeyBytes.byteOffset,
551
+ publicKeyBytes.byteOffset +
552
+ publicKeyBytes.byteLength,
494
553
  ),
495
- )
496
- ).pipe(
497
- Fx.chain((publicKeyBytes) =>
498
- Fx.from({
499
- ok: async () => {
500
- const deviceType =
501
- params.deviceType ?? "single-device";
502
- const backedUp = params.backedUp ?? false;
503
-
504
- const db = authDb(ctx, ctx.auth.config);
505
- await db.accounts.create({
506
- userId,
507
- provider: provider.id,
508
- providerAccountId: credentialId,
509
- });
510
-
511
- await mutatePasskeyInsert(ctx, {
512
- userId,
513
- credentialId,
514
- publicKey: publicKeyBytes.buffer.slice(
515
- publicKeyBytes.byteOffset,
516
- publicKeyBytes.byteOffset +
517
- publicKeyBytes.byteLength,
518
- ),
519
- algorithm,
520
- counter: authData.signatureCounter,
521
- transports: params.transports,
522
- deviceType,
523
- backedUp,
524
- name: params.passkeyName,
525
- createdAt: Date.now(),
526
- });
527
-
528
- const signInResult = await callSignIn(ctx, {
529
- userId,
530
- generateTokens: true,
531
- });
532
-
533
- return {
534
- kind: "signedIn" as const,
535
- signedIn: signInResult,
536
- };
537
- },
538
- err: () => new AuthError("INTERNAL_ERROR"),
539
- }),
540
- ),
541
- );
542
- }),
543
- );
544
- }),
545
- ),
554
+ algorithm,
555
+ counter: authData.signatureCounter,
556
+ transports: params.transports,
557
+ deviceType,
558
+ backedUp,
559
+ name: params.passkeyName,
560
+ createdAt: Date.now(),
561
+ });
562
+
563
+ const signInResult = await callSignIn(ctx, {
564
+ userId,
565
+ generateTokens: true,
566
+ });
567
+
568
+ return {
569
+ kind: "signedIn" as const,
570
+ signedIn: signInResult,
571
+ };
572
+ },
573
+ err: () =>
574
+ Cv.error({
575
+ code: "INTERNAL_ERROR",
576
+ message: "An unexpected error occurred.",
577
+ }),
578
+ }),
579
+ ),
580
+ );
581
+ }),
582
+ );
583
+ }),
546
584
  ),
547
585
  ),
548
- authOptions: (_) =>
549
- resolveRpOptionsFx(provider).pipe(
550
- Fx.chain((rp) => {
551
- const challenge = new Uint8Array(32);
552
- crypto.getRandomValues(challenge);
553
- const challengeHash = encodeBase64urlNoPadding(
554
- new Uint8Array(sha256(challenge)),
555
- );
556
-
557
- return Fx.from({
558
- ok: async () => {
559
- const verifier = await callVerifier(ctx);
560
- await callVerifierSignature(ctx, {
561
- verifier,
562
- signature: challengeHash,
563
- });
564
-
565
- let allowCredentials:
566
- | Array<{
567
- type: string;
568
- id: string;
569
- transports?: string[];
570
- }>
571
- | undefined;
572
- if (params.email) {
573
- const user = await queryUserByVerifiedEmail(
586
+ ),
587
+ authOptions: (_) =>
588
+ resolveRpOptionsFx(provider).pipe(
589
+ Fx.chain((rp) => {
590
+ const challenge = new Uint8Array(32);
591
+ crypto.getRandomValues(challenge);
592
+ const challengeHash = encodeBase64urlNoPadding(
593
+ new Uint8Array(sha256(challenge)),
594
+ );
595
+
596
+ return Fx.from({
597
+ ok: async () => {
598
+ const verifier = await callVerifier(ctx);
599
+ await callVerifierSignature(ctx, {
600
+ verifier,
601
+ signature: challengeHash,
602
+ });
603
+
604
+ let allowCredentials:
605
+ | Array<{
606
+ type: string;
607
+ id: string;
608
+ transports?: string[];
609
+ }>
610
+ | undefined;
611
+ if (params.email) {
612
+ const user = await queryUserByVerifiedEmail(
613
+ ctx,
614
+ params.email,
615
+ );
616
+ if (user) {
617
+ const passkeys = await queryPasskeysByUserId(
574
618
  ctx,
575
- params.email,
619
+ user._id,
576
620
  );
577
- if (user) {
578
- const passkeys = await queryPasskeysByUserId(
579
- ctx,
580
- user._id,
581
- );
582
- if (passkeys.length > 0) {
583
- allowCredentials = passkeys.map((pk) => ({
584
- type: "public-key",
585
- id: pk.credentialId,
586
- transports: pk.transports,
587
- }));
588
- }
621
+ if (passkeys.length > 0) {
622
+ allowCredentials = passkeys.map((pk) => ({
623
+ type: "public-key",
624
+ id: pk.credentialId,
625
+ transports: pk.transports,
626
+ }));
589
627
  }
590
628
  }
591
-
592
- const options: Record<string, any> = {
593
- challenge: encodeBase64urlNoPadding(challenge),
594
- timeout: rp.challengeExpirationMs,
595
- rpId: rp.rpId,
596
- userVerification: rp.userVerification,
597
- };
598
-
599
- if (allowCredentials) {
600
- options.allowCredentials = allowCredentials;
601
- }
602
-
603
- return {
604
- kind: "passkeyOptions" as const,
605
- options,
606
- verifier,
607
- };
608
- },
609
- err: () => new AuthError("INTERNAL_ERROR"),
610
- });
611
- }),
612
- ),
613
- authVerify: (_) =>
614
- Fx.zip(
615
- resolveRpOptionsFx(provider),
616
- requirePasskeyVerifierFx(args.verifier),
617
- ).pipe(
618
- Fx.chain(([rp, verifier]) => {
619
- const clientDataJSON = decodeBase64urlIgnorePadding(
620
- params.clientDataJSON,
621
- );
622
- const clientData = parseClientDataJSON(clientDataJSON);
623
-
624
- const verifiedClientDataFx = Fx.succeed(clientData).pipe(
625
- Fx.chain(
626
- verifyClientDataType(ClientDataType.Get, "webauthn.get"),
627
- ),
628
- Fx.chain(verifyOrigin(rp)),
629
- Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),
630
- Fx.chain(() =>
631
- params.credentialId != null
632
- ? Fx.succeed(params.credentialId as string)
633
- : Fx.fail(
634
- new AuthError(
635
- "PASSKEY_UNKNOWN_CREDENTIAL",
636
- "Missing credential ID",
637
- ),
638
- ),
639
- ),
640
- );
641
-
642
- return verifiedClientDataFx.pipe(
643
- Fx.chain((credentialId) =>
644
- Fx.from({
645
- ok: () => queryPasskeyByCredentialId(ctx, credentialId),
646
- err: () => new AuthError("PASSKEY_UNKNOWN_CREDENTIAL"),
647
- }).pipe(
648
- Fx.chain((passkey) =>
649
- passkey
650
- ? Fx.succeed(passkey)
651
- : Fx.fail(
652
- new AuthError(
653
- "PASSKEY_UNKNOWN_CREDENTIAL",
654
- "Unknown credential",
655
- ),
656
- ),
657
- ),
629
+ }
630
+
631
+ const options: Record<string, any> = {
632
+ challenge: encodeBase64urlNoPadding(challenge),
633
+ timeout: rp.challengeExpirationMs,
634
+ rpId: rp.rpId,
635
+ userVerification: rp.userVerification,
636
+ };
637
+
638
+ if (allowCredentials) {
639
+ options.allowCredentials = allowCredentials;
640
+ }
641
+
642
+ return {
643
+ kind: "passkeyOptions" as const,
644
+ options,
645
+ verifier,
646
+ };
647
+ },
648
+ err: () =>
649
+ Cv.error({
650
+ code: "INTERNAL_ERROR",
651
+ message: "An unexpected error occurred.",
652
+ }),
653
+ });
654
+ }),
655
+ ),
656
+ authVerify: (_) =>
657
+ Fx.zip(
658
+ resolveRpOptionsFx(provider),
659
+ requirePasskeyVerifierFx(args.verifier),
660
+ ).pipe(
661
+ Fx.chain(([rp, verifier]) => {
662
+ const clientDataJSON = decodeBase64urlIgnorePadding(
663
+ params.clientDataJSON,
664
+ );
665
+ const clientData = parseClientDataJSON(clientDataJSON);
666
+
667
+ const verifiedClientDataFx = Fx.succeed(clientData).pipe(
668
+ Fx.chain(
669
+ verifyClientDataType(ClientDataType.Get, "webauthn.get"),
670
+ ),
671
+ Fx.chain(verifyOrigin(rp)),
672
+ Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),
673
+ Fx.chain(() =>
674
+ params.credentialId != null
675
+ ? Fx.succeed(params.credentialId as string)
676
+ : Cv.fail({
677
+ code: "PASSKEY_UNKNOWN_CREDENTIAL",
678
+ message: "Missing credential ID",
679
+ }),
680
+ ),
681
+ );
682
+
683
+ return verifiedClientDataFx.pipe(
684
+ Fx.chain((credentialId) =>
685
+ Fx.from({
686
+ ok: () => queryPasskeyByCredentialId(ctx, credentialId),
687
+ err: () =>
688
+ Cv.error({
689
+ code: "PASSKEY_UNKNOWN_CREDENTIAL",
690
+ message: "Unknown passkey credential.",
691
+ }),
692
+ }).pipe(
693
+ Fx.chain((passkey) =>
694
+ passkey
695
+ ? Fx.succeed(passkey)
696
+ : Cv.fail({
697
+ code: "PASSKEY_UNKNOWN_CREDENTIAL",
698
+ message: "Unknown credential",
699
+ }),
658
700
  ),
659
701
  ),
660
- Fx.chain((passkey) => {
661
- const authenticatorDataBytes = decodeBase64urlIgnorePadding(
662
- params.authenticatorData,
663
- );
664
- const authenticatorData = parseAuthenticatorData(
665
- authenticatorDataBytes,
666
- );
667
-
668
- const signature = decodeBase64urlIgnorePadding(
669
- params.signature,
670
- );
671
- const signatureMessage = createAssertionSignatureMessage(
672
- authenticatorDataBytes,
673
- clientDataJSON,
674
- );
675
- const messageHash = sha256(signatureMessage);
676
-
677
- const checkedAuthenticatorFx = Fx.succeed(
678
- authenticatorData,
679
- ).pipe(
680
- Fx.chain(verifyRpId(rp.rpId)),
681
- Fx.chain(verifyUserFlags(rp)),
682
- );
702
+ ),
703
+ Fx.chain((passkey) => {
704
+ const authenticatorDataBytes = decodeBase64urlIgnorePadding(
705
+ params.authenticatorData,
706
+ );
707
+ const authenticatorData = parseAuthenticatorData(
708
+ authenticatorDataBytes,
709
+ );
710
+
711
+ const signature = decodeBase64urlIgnorePadding(
712
+ params.signature,
713
+ );
714
+ const signatureMessage = createAssertionSignatureMessage(
715
+ authenticatorDataBytes,
716
+ clientDataJSON,
717
+ );
718
+ const messageHash = sha256(signatureMessage);
719
+
720
+ const checkedAuthenticatorFx = Fx.succeed(
721
+ authenticatorData,
722
+ ).pipe(
723
+ Fx.chain(verifyRpId(rp.rpId)),
724
+ Fx.chain(verifyUserFlags(rp)),
725
+ );
726
+
727
+ const signatureVerifiedFx = checkedAuthenticatorFx.pipe(
728
+ Fx.chain(() => {
729
+ const storedPublicKeyBytes = new Uint8Array(
730
+ passkey.publicKey,
731
+ );
732
+ const algorithmHandlers: Record<
733
+ number,
734
+ (() => FxType<void, ConvexError<any>>) | undefined
735
+ > = {
736
+ [coseAlgorithmES256]: () => {
737
+ const ecPublicKey = decodeSEC1PublicKey(
738
+ p256,
739
+ storedPublicKeyBytes,
740
+ );
741
+ const ecdsaSignature =
742
+ decodePKIXECDSASignature(signature);
743
+ const valid = verifyECDSASignature(
744
+ ecPublicKey,
745
+ messageHash,
746
+ ecdsaSignature,
747
+ );
748
+ return valid
749
+ ? Fx.succeed(undefined as void)
750
+ : Cv.fail({
751
+ code: "PASSKEY_INVALID_SIGNATURE",
752
+ message: "Invalid passkey signature.",
753
+ });
754
+ },
755
+ [coseAlgorithmRS256]: () => {
756
+ const rsaPublicKey =
757
+ decodePKCS1RSAPublicKey(storedPublicKeyBytes);
758
+ const valid = verifyRSASSAPKCS1v15Signature(
759
+ rsaPublicKey,
760
+ sha256ObjectIdentifier,
761
+ messageHash,
762
+ signature,
763
+ );
764
+ return valid
765
+ ? Fx.succeed(undefined as void)
766
+ : Cv.fail({
767
+ code: "PASSKEY_INVALID_SIGNATURE",
768
+ message: "Invalid passkey signature.",
769
+ });
770
+ },
771
+ };
772
+
773
+ const handler = algorithmHandlers[passkey.algorithm];
774
+ return handler
775
+ ? handler()
776
+ : Cv.fail({
777
+ code: "PASSKEY_UNSUPPORTED_ALGORITHM",
778
+ message: `Unsupported algorithm: ${passkey.algorithm}`,
779
+ });
780
+ }),
781
+ );
782
+
783
+ const counterValidatedFx = signatureVerifiedFx.pipe(
784
+ Fx.chain(() =>
785
+ passkey.counter !== 0 &&
786
+ authenticatorData.signatureCounter !== 0 &&
787
+ authenticatorData.signatureCounter <= passkey.counter
788
+ ? Cv.fail({
789
+ code: "PASSKEY_COUNTER_ERROR",
790
+ message:
791
+ "Authenticator counter did not increase — possible credential cloning detected.",
792
+ })
793
+ : Fx.succeed(authenticatorData),
794
+ ),
795
+ );
796
+
797
+ return counterValidatedFx.pipe(
798
+ Fx.chain(() =>
799
+ Fx.from({
800
+ ok: async () => {
801
+ await mutatePasskeyUpdateCounter(
802
+ ctx,
803
+ passkey._id,
804
+ authenticatorData.signatureCounter,
805
+ Date.now(),
806
+ );
683
807
 
684
- const signatureVerifiedFx = checkedAuthenticatorFx.pipe(
685
- Fx.chain(() => {
686
- const storedPublicKeyBytes = new Uint8Array(
687
- passkey.publicKey,
688
- );
689
- const algorithmHandlers: Record<
690
- number,
691
- (() => FxType<void, AuthError>) | undefined
692
- > = {
693
- [coseAlgorithmES256]: () => {
694
- const ecPublicKey = decodeSEC1PublicKey(
695
- p256,
696
- storedPublicKeyBytes,
697
- );
698
- const ecdsaSignature =
699
- decodePKIXECDSASignature(signature);
700
- const valid = verifyECDSASignature(
701
- ecPublicKey,
702
- messageHash,
703
- ecdsaSignature,
704
- );
705
- return valid
706
- ? Fx.succeed(undefined as void)
707
- : Fx.fail(
708
- new AuthError("PASSKEY_INVALID_SIGNATURE"),
709
- );
710
- },
711
- [coseAlgorithmRS256]: () => {
712
- const rsaPublicKey =
713
- decodePKCS1RSAPublicKey(storedPublicKeyBytes);
714
- const valid = verifyRSASSAPKCS1v15Signature(
715
- rsaPublicKey,
716
- sha256ObjectIdentifier,
717
- messageHash,
718
- signature,
719
- );
720
- return valid
721
- ? Fx.succeed(undefined as void)
722
- : Fx.fail(
723
- new AuthError("PASSKEY_INVALID_SIGNATURE"),
724
- );
725
- },
726
- };
727
-
728
- const handler = algorithmHandlers[passkey.algorithm];
729
- return handler
730
- ? handler()
731
- : Fx.fail(
732
- new AuthError(
733
- "PASSKEY_UNSUPPORTED_ALGORITHM",
734
- `Unsupported algorithm: ${passkey.algorithm}`,
735
- ),
736
- );
808
+ const signInResult = await callSignIn(ctx, {
809
+ userId: passkey.userId,
810
+ generateTokens: true,
811
+ });
812
+
813
+ return {
814
+ kind: "signedIn" as const,
815
+ signedIn: signInResult,
816
+ };
817
+ },
818
+ err: () =>
819
+ Cv.error({
820
+ code: "INTERNAL_ERROR",
821
+ message: "An unexpected error occurred.",
822
+ }),
737
823
  }),
738
- );
739
-
740
- const counterValidatedFx = signatureVerifiedFx.pipe(
741
- Fx.chain(() =>
742
- passkey.counter !== 0 &&
743
- authenticatorData.signatureCounter !== 0 &&
744
- authenticatorData.signatureCounter <= passkey.counter
745
- ? Fx.fail(new AuthError("PASSKEY_COUNTER_ERROR"))
746
- : Fx.succeed(authenticatorData),
747
- ),
748
- );
749
-
750
- return counterValidatedFx.pipe(
751
- Fx.chain(() =>
752
- Fx.from({
753
- ok: async () => {
754
- await mutatePasskeyUpdateCounter(
755
- ctx,
756
- passkey._id,
757
- authenticatorData.signatureCounter,
758
- Date.now(),
759
- );
760
-
761
- const signInResult = await callSignIn(ctx, {
762
- userId: passkey.userId,
763
- generateTokens: true,
764
- });
765
-
766
- return {
767
- kind: "signedIn" as const,
768
- signedIn: signInResult,
769
- };
770
- },
771
- err: () => new AuthError("INTERNAL_ERROR"),
772
- }),
773
- ),
774
- );
775
- }),
776
- );
777
- }),
778
- ),
779
- },
780
- );
824
+ ),
825
+ );
826
+ }),
827
+ );
828
+ }),
829
+ ),
830
+ });
781
831
  return flowFx;
782
832
  }),
783
833
  );