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

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 (114) hide show
  1. package/dist/bin.cjs +467 -64
  2. package/dist/client/index.d.ts +127 -0
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +424 -1
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +56 -1
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/api.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +141 -3
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/convex.config.d.ts.map +1 -1
  12. package/dist/component/convex.config.js +2 -0
  13. package/dist/component/convex.config.js.map +1 -1
  14. package/dist/component/index.d.ts +5 -4
  15. package/dist/component/index.d.ts.map +1 -1
  16. package/dist/component/index.js +4 -3
  17. package/dist/component/index.js.map +1 -1
  18. package/dist/component/portalBridge.d.ts +80 -0
  19. package/dist/component/portalBridge.d.ts.map +1 -0
  20. package/dist/component/portalBridge.js +102 -0
  21. package/dist/component/portalBridge.js.map +1 -0
  22. package/dist/component/public.d.ts +353 -9
  23. package/dist/component/public.d.ts.map +1 -1
  24. package/dist/component/public.js +328 -33
  25. package/dist/component/public.js.map +1 -1
  26. package/dist/component/schema.d.ts +168 -9
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +113 -7
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/providers/passkey.d.ts +20 -0
  31. package/dist/providers/passkey.d.ts.map +1 -0
  32. package/dist/providers/passkey.js +32 -0
  33. package/dist/providers/passkey.js.map +1 -0
  34. package/dist/providers/totp.d.ts +14 -0
  35. package/dist/providers/totp.d.ts.map +1 -0
  36. package/dist/providers/totp.js +23 -0
  37. package/dist/providers/totp.js.map +1 -0
  38. package/dist/server/convex-auth.d.ts +296 -0
  39. package/dist/server/convex-auth.d.ts.map +1 -0
  40. package/dist/server/convex-auth.js +480 -0
  41. package/dist/server/convex-auth.js.map +1 -0
  42. package/dist/server/email-templates.d.ts +18 -0
  43. package/dist/server/email-templates.d.ts.map +1 -0
  44. package/dist/server/email-templates.js +74 -0
  45. package/dist/server/email-templates.js.map +1 -0
  46. package/dist/server/implementation/apiKey.d.ts +74 -0
  47. package/dist/server/implementation/apiKey.d.ts.map +1 -0
  48. package/dist/server/implementation/apiKey.js +140 -0
  49. package/dist/server/implementation/apiKey.js.map +1 -0
  50. package/dist/server/implementation/index.d.ts +169 -7
  51. package/dist/server/implementation/index.d.ts.map +1 -1
  52. package/dist/server/implementation/index.js +220 -5
  53. package/dist/server/implementation/index.js.map +1 -1
  54. package/dist/server/implementation/passkey.d.ts +33 -0
  55. package/dist/server/implementation/passkey.d.ts.map +1 -0
  56. package/dist/server/implementation/passkey.js +450 -0
  57. package/dist/server/implementation/passkey.js.map +1 -0
  58. package/dist/server/implementation/redirects.d.ts.map +1 -1
  59. package/dist/server/implementation/redirects.js +4 -9
  60. package/dist/server/implementation/redirects.js.map +1 -1
  61. package/dist/server/implementation/signIn.d.ts +13 -0
  62. package/dist/server/implementation/signIn.d.ts.map +1 -1
  63. package/dist/server/implementation/signIn.js +29 -15
  64. package/dist/server/implementation/signIn.js.map +1 -1
  65. package/dist/server/implementation/totp.d.ts +40 -0
  66. package/dist/server/implementation/totp.d.ts.map +1 -0
  67. package/dist/server/implementation/totp.js +211 -0
  68. package/dist/server/implementation/totp.js.map +1 -0
  69. package/dist/server/index.d.ts +26 -2
  70. package/dist/server/index.d.ts.map +1 -1
  71. package/dist/server/index.js +63 -16
  72. package/dist/server/index.js.map +1 -1
  73. package/dist/server/portal-email.d.ts +19 -0
  74. package/dist/server/portal-email.d.ts.map +1 -0
  75. package/dist/server/portal-email.js +89 -0
  76. package/dist/server/portal-email.js.map +1 -0
  77. package/dist/server/provider_utils.d.ts +3 -1
  78. package/dist/server/provider_utils.d.ts.map +1 -1
  79. package/dist/server/provider_utils.js +39 -1
  80. package/dist/server/provider_utils.js.map +1 -1
  81. package/dist/server/types.d.ts +263 -4
  82. package/dist/server/types.d.ts.map +1 -1
  83. package/dist/server/version.d.ts +2 -0
  84. package/dist/server/version.d.ts.map +1 -0
  85. package/dist/server/version.js +3 -0
  86. package/dist/server/version.js.map +1 -0
  87. package/package.json +7 -3
  88. package/src/cli/index.ts +49 -7
  89. package/src/cli/portal-link.ts +112 -0
  90. package/src/cli/portal-upload.ts +411 -0
  91. package/src/cli/utils.ts +248 -0
  92. package/src/client/index.ts +489 -1
  93. package/src/component/_generated/api.ts +72 -1
  94. package/src/component/_generated/component.ts +241 -4
  95. package/src/component/convex.config.ts +3 -0
  96. package/src/component/index.ts +8 -3
  97. package/src/component/portalBridge.ts +116 -0
  98. package/src/component/public.ts +373 -37
  99. package/src/component/schema.ts +122 -7
  100. package/src/providers/passkey.ts +35 -0
  101. package/src/providers/totp.ts +26 -0
  102. package/src/server/convex-auth.ts +602 -0
  103. package/src/server/email-templates.ts +77 -0
  104. package/src/server/implementation/apiKey.ts +185 -0
  105. package/src/server/implementation/index.ts +301 -8
  106. package/src/server/implementation/passkey.ts +650 -0
  107. package/src/server/implementation/redirects.ts +4 -11
  108. package/src/server/implementation/signIn.ts +41 -13
  109. package/src/server/implementation/totp.ts +366 -0
  110. package/src/server/index.ts +98 -34
  111. package/src/server/portal-email.ts +95 -0
  112. package/src/server/provider_utils.ts +42 -1
  113. package/src/server/types.ts +285 -4
  114. package/src/server/version.ts +2 -0
@@ -20,6 +20,8 @@ export interface Storage {
20
20
  type SignInResult = {
21
21
  signingIn: boolean;
22
22
  redirect?: URL;
23
+ totpRequired?: boolean;
24
+ verifier?: string;
23
25
  };
24
26
  /** Reactive auth state snapshot returned by `auth.state` and `auth.onChange`. */
25
27
  export type AuthState = {
@@ -98,6 +100,131 @@ export declare function client(options: ClientOptions): {
98
100
  signIn: (provider?: string, args?: FormData | Record<string, Value>) => Promise<SignInResult>;
99
101
  signOut: () => Promise<void>;
100
102
  onChange: (cb: (state: AuthState) => void) => (() => void);
103
+ /** Passkey (WebAuthn) authentication helpers. */
104
+ passkey: {
105
+ /**
106
+ * Check if WebAuthn passkeys are supported in the current environment.
107
+ */
108
+ isSupported: () => boolean;
109
+ /**
110
+ * Check if conditional UI (autofill-assisted passkey sign-in) is supported.
111
+ *
112
+ * ```ts
113
+ * if (await auth.passkey.isAutofillSupported()) {
114
+ * auth.passkey.authenticate({ autofill: true });
115
+ * }
116
+ * ```
117
+ */
118
+ isAutofillSupported: () => Promise<boolean>;
119
+ /**
120
+ * Register a new passkey for the current or new user.
121
+ *
122
+ * Performs the full two-round-trip WebAuthn registration ceremony:
123
+ * 1. Requests creation options from the server (challenge, RP info)
124
+ * 2. Calls `navigator.credentials.create()` with the options
125
+ * 3. Sends the attestation back to the server for verification
126
+ * 4. Server creates user + account + passkey records and returns tokens
127
+ *
128
+ * Works in both SPA and proxy (SSR) modes.
129
+ *
130
+ * ```ts
131
+ * await auth.passkey.register({ name: "MacBook Touch ID" });
132
+ * ```
133
+ *
134
+ * @param opts.name - Friendly name for this passkey
135
+ * @param opts.email - Email to associate with the new account
136
+ * @param opts.userName - Username for the credential (defaults to email)
137
+ * @param opts.userDisplayName - Display name for the credential
138
+ * @returns `{ signingIn: true }` on success
139
+ */
140
+ register: (opts?: {
141
+ name?: string;
142
+ email?: string;
143
+ userName?: string;
144
+ userDisplayName?: string;
145
+ }) => Promise<SignInResult>;
146
+ /**
147
+ * Authenticate with an existing passkey.
148
+ *
149
+ * Performs the full two-round-trip WebAuthn authentication ceremony:
150
+ * 1. Requests assertion options from the server (challenge, allowed credentials)
151
+ * 2. Calls `navigator.credentials.get()` with the options
152
+ * 3. Sends the assertion back to the server for signature verification
153
+ * 4. Server verifies signature, updates counter, creates session, returns tokens
154
+ *
155
+ * Works in both SPA and proxy (SSR) modes.
156
+ *
157
+ * ```ts
158
+ * // Discoverable credential (no email needed)
159
+ * await auth.passkey.authenticate();
160
+ *
161
+ * // Scoped to a specific user's credentials
162
+ * await auth.passkey.authenticate({ email: "user@example.com" });
163
+ *
164
+ * // Autofill-assisted (conditional UI)
165
+ * await auth.passkey.authenticate({ autofill: true });
166
+ * ```
167
+ *
168
+ * @param opts.email - Scope to credentials for this email's user
169
+ * @param opts.autofill - Use conditional mediation (autofill UI)
170
+ * @returns `{ signingIn: true }` on success
171
+ */
172
+ authenticate: (opts?: {
173
+ email?: string;
174
+ autofill?: boolean;
175
+ }) => Promise<SignInResult>;
176
+ };
177
+ /** TOTP two-factor authentication helpers. */
178
+ totp: {
179
+ /**
180
+ * Start TOTP enrollment. Must be authenticated.
181
+ *
182
+ * Returns a URI for QR code display and a base32 secret for manual entry.
183
+ *
184
+ * ```ts
185
+ * const setup = await auth.totp.setup();
186
+ * // Display QR code from setup.uri
187
+ * // Or show setup.secret for manual entry
188
+ * ```
189
+ */
190
+ setup: (opts?: {
191
+ name?: string;
192
+ accountName?: string;
193
+ }) => Promise<{
194
+ uri: string;
195
+ secret: string;
196
+ verifier: string;
197
+ totpId: string;
198
+ }>;
199
+ /**
200
+ * Complete TOTP enrollment by verifying the first code from the authenticator app.
201
+ *
202
+ * ```ts
203
+ * await auth.totp.confirm({ code: "123456", verifier: setup.verifier, totpId: setup.totpId });
204
+ * ```
205
+ */
206
+ confirm: (opts: {
207
+ code: string;
208
+ verifier: string;
209
+ totpId: string;
210
+ }) => Promise<void>;
211
+ /**
212
+ * Complete 2FA verification during sign-in.
213
+ *
214
+ * Called after a credentials sign-in returns `totpRequired: true`.
215
+ *
216
+ * ```ts
217
+ * const result = await auth.signIn("password", { email, password });
218
+ * if (result.totpRequired) {
219
+ * await auth.totp.verify({ code: "123456", verifier: result.verifier! });
220
+ * }
221
+ * ```
222
+ */
223
+ verify: (opts: {
224
+ code: string;
225
+ verifier: string;
226
+ }) => Promise<void>;
227
+ };
101
228
  };
102
229
  export {};
103
230
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEtC;;;;GAIG;AACH,UAAU,eAAe;IACvB,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7C,OAAO,CACL,UAAU,EAAE,CAAC,IAAI,EAAE;QACjB,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,KAAK,IAAI,GAC5C,IAAI,CAAC;IACR,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,gEAAgE;AAChE,MAAM,WAAW,OAAO;IACtB,OAAO,CACL,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAOD,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,kCAAkC;AAClC,MAAM,MAAM,aAAa,GAAG;IAC1B,iEAAiE;IACjE,MAAM,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAyBF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa;IAoazC,mCAAmC;oBACtB,SAAS;wBAlPX,MAAM,SACV,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KACtC,OAAO,CAAC,YAAY,CAAC;;mBA4LF,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC;EA2DhE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEtC;;;;GAIG;AACH,UAAU,eAAe;IACvB,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7C,OAAO,CACL,UAAU,EAAE,CAAC,IAAI,EAAE;QACjB,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,eAAe,EAAE,OAAO,KAAK,IAAI,GAC5C,IAAI,CAAC;IACR,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,gEAAgE;AAChE,MAAM,WAAW,OAAO;IACtB,OAAO,CACL,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAOD,KAAK,YAAY,GAAG;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,kCAAkC;AAClC,MAAM,MAAM,aAAa,GAAG;IAC1B,iEAAiE;IACjE,MAAM,EAAE,eAAe,CAAC;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAyBF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,aAAa;IAs4BzC,mCAAmC;oBACtB,SAAS;wBA7sBX,MAAM,SACV,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KACtC,OAAO,CAAC,YAAY,CAAC;;mBAkMF,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC;IA+gB7D,iDAAiD;;QA7bjD;;WAEG;2BACc,OAAO;QAOxB;;;;;;;;WAQG;mCAC4B,OAAO,CAAC,OAAO,CAAC;QAe/C;;;;;;;;;;;;;;;;;;;;WAoBG;0BAEM;YACL,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,QAAQ,CAAC,EAAE,MAAM,CAAC;YAClB,eAAe,CAAC,EAAE,MAAM,CAAC;SAC1B,KACA,OAAO,CAAC,YAAY,CAAC;QAuHxB;;;;;;;;;;;;;;;;;;;;;;;;;WAyBG;8BAEM;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;SAAE,KAC5C,OAAO,CAAC,YAAY,CAAC;;IA8OxB,8CAA8C;;QAtI9C;;;;;;;;;;WAUG;uBAEM;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,KAC7C,OAAO,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAoB7E;;;;;;WAMG;wBACmB;YACpB,IAAI,EAAE,MAAM,CAAC;YACb,QAAQ,EAAE,MAAM,CAAC;YACjB,MAAM,EAAE,MAAM,CAAC;SAChB,KAAG,OAAO,CAAC,IAAI,CAAC;QAkCjB;;;;;;;;;;;WAWG;uBACkB;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAG,OAAO,CAAC,IAAI,CAAC;;EA+C1E"}
@@ -137,6 +137,13 @@ export function client(options) {
137
137
  isLoading = false;
138
138
  const changed = updateSnapshot();
139
139
  if (hadPendingLoad || changed) {
140
+ // Re-sync the Convex client so it picks up the new token immediately.
141
+ // Without this, the initial convex.setAuth(fetchAccessToken) from
142
+ // initialization never re-polls and queries run unauthenticated after
143
+ // magic link code exchange.
144
+ if (!proxy) {
145
+ convex.setAuth(fetchAccessToken);
146
+ }
140
147
  notify();
141
148
  }
142
149
  };
@@ -212,6 +219,9 @@ export function client(options) {
212
219
  }
213
220
  return { signingIn: false, redirect: redirectUrl };
214
221
  }
222
+ if (result.totpRequired) {
223
+ return { signingIn: false, totpRequired: true, verifier: result.verifier };
224
+ }
215
225
  if (result.tokens !== undefined) {
216
226
  // Proxy returns { token, refreshToken: "dummy" }.
217
227
  // Store JWT in memory only — real refresh token is in httpOnly cookie.
@@ -239,6 +249,9 @@ export function client(options) {
239
249
  }
240
250
  return { signingIn: false, redirect: redirectUrl };
241
251
  }
252
+ if (result.totpRequired) {
253
+ return { signingIn: false, totpRequired: true, verifier: result.verifier };
254
+ }
242
255
  if (result.tokens !== undefined) {
243
256
  await setToken({
244
257
  shouldStore: true,
@@ -408,9 +421,415 @@ export function client(options) {
408
421
  }
409
422
  else {
410
423
  // SPA mode: hydrate from localStorage, then handle OAuth code flow.
411
- void hydrateFromStorage().then(() => handleCodeFlow());
424
+ void hydrateFromStorage().then(() => handleCodeFlow().catch((error) => {
425
+ console.error("[convex-auth] Code exchange failed:", error);
426
+ }));
412
427
  }
413
428
  }
429
+ // ---------------------------------------------------------------------------
430
+ // Passkey helpers
431
+ // ---------------------------------------------------------------------------
432
+ /**
433
+ * Base64url encode/decode helpers for the WebAuthn credential API.
434
+ * These run client-side only (browser context).
435
+ */
436
+ const base64urlEncode = (buffer) => {
437
+ const bytes = new Uint8Array(buffer);
438
+ let binary = "";
439
+ for (let i = 0; i < bytes.byteLength; i++) {
440
+ binary += String.fromCharCode(bytes[i]);
441
+ }
442
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
443
+ };
444
+ const base64urlDecode = (str) => {
445
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
446
+ const binary = atob(padded);
447
+ const bytes = new Uint8Array(binary.length);
448
+ for (let i = 0; i < binary.length; i++) {
449
+ bytes[i] = binary.charCodeAt(i);
450
+ }
451
+ return bytes;
452
+ };
453
+ const passkey = {
454
+ /**
455
+ * Check if WebAuthn passkeys are supported in the current environment.
456
+ */
457
+ isSupported: () => {
458
+ return (typeof window !== "undefined" &&
459
+ typeof window.PublicKeyCredential !== "undefined");
460
+ },
461
+ /**
462
+ * Check if conditional UI (autofill-assisted passkey sign-in) is supported.
463
+ *
464
+ * ```ts
465
+ * if (await auth.passkey.isAutofillSupported()) {
466
+ * auth.passkey.authenticate({ autofill: true });
467
+ * }
468
+ * ```
469
+ */
470
+ isAutofillSupported: async () => {
471
+ if (typeof window === "undefined")
472
+ return false;
473
+ if (typeof window.PublicKeyCredential === "undefined")
474
+ return false;
475
+ if (typeof window.PublicKeyCredential.isConditionalMediationAvailable !== "function") {
476
+ return false;
477
+ }
478
+ return window.PublicKeyCredential.isConditionalMediationAvailable();
479
+ },
480
+ /**
481
+ * Register a new passkey for the current or new user.
482
+ *
483
+ * Performs the full two-round-trip WebAuthn registration ceremony:
484
+ * 1. Requests creation options from the server (challenge, RP info)
485
+ * 2. Calls `navigator.credentials.create()` with the options
486
+ * 3. Sends the attestation back to the server for verification
487
+ * 4. Server creates user + account + passkey records and returns tokens
488
+ *
489
+ * Works in both SPA and proxy (SSR) modes.
490
+ *
491
+ * ```ts
492
+ * await auth.passkey.register({ name: "MacBook Touch ID" });
493
+ * ```
494
+ *
495
+ * @param opts.name - Friendly name for this passkey
496
+ * @param opts.email - Email to associate with the new account
497
+ * @param opts.userName - Username for the credential (defaults to email)
498
+ * @param opts.userDisplayName - Display name for the credential
499
+ * @returns `{ signingIn: true }` on success
500
+ */
501
+ register: async (opts) => {
502
+ const phase1Params = {
503
+ flow: "register-options",
504
+ email: opts?.email,
505
+ userName: opts?.userName,
506
+ userDisplayName: opts?.userDisplayName,
507
+ };
508
+ // Phase 1: Get registration options from server
509
+ let phase1Result;
510
+ if (proxy) {
511
+ phase1Result = await proxyFetch({
512
+ action: "auth:signIn",
513
+ args: { provider: "passkey", params: phase1Params },
514
+ });
515
+ }
516
+ else {
517
+ phase1Result = await convex.action("auth:signIn", {
518
+ provider: "passkey",
519
+ params: phase1Params,
520
+ });
521
+ }
522
+ if (!phase1Result.options) {
523
+ throw new Error("Server did not return passkey registration options");
524
+ }
525
+ const options = phase1Result.options;
526
+ // Convert base64url strings to ArrayBuffers for the credential API
527
+ const createOptions = {
528
+ publicKey: {
529
+ rp: options.rp,
530
+ user: {
531
+ id: base64urlDecode(options.user.id).buffer,
532
+ name: options.user.name,
533
+ displayName: options.user.displayName,
534
+ },
535
+ challenge: base64urlDecode(options.challenge).buffer,
536
+ pubKeyCredParams: options.pubKeyCredParams,
537
+ timeout: options.timeout,
538
+ attestation: options.attestation,
539
+ authenticatorSelection: options.authenticatorSelection,
540
+ excludeCredentials: (options.excludeCredentials ?? []).map((cred) => ({
541
+ type: cred.type ?? "public-key",
542
+ id: base64urlDecode(cred.id).buffer,
543
+ transports: cred.transports,
544
+ })),
545
+ },
546
+ };
547
+ // Phase 2: Create credential via browser API
548
+ const credential = (await navigator.credentials.create(createOptions));
549
+ if (!credential) {
550
+ throw new Error("Passkey registration was cancelled");
551
+ }
552
+ const response = credential.response;
553
+ // Extract transports if available
554
+ const transports = typeof response.getTransports === "function"
555
+ ? response.getTransports()
556
+ : undefined;
557
+ const phase2Params = {
558
+ flow: "register-verify",
559
+ clientDataJSON: base64urlEncode(response.clientDataJSON),
560
+ attestationObject: base64urlEncode(response.attestationObject),
561
+ transports,
562
+ passkeyName: opts?.name,
563
+ email: opts?.email,
564
+ };
565
+ // Phase 3: Send attestation to server for verification
566
+ let phase2Result;
567
+ if (proxy) {
568
+ // In proxy mode the verifier is stored in an httpOnly cookie by the proxy.
569
+ // We pass it back explicitly so the proxy can forward it to Convex.
570
+ phase2Result = await proxyFetch({
571
+ action: "auth:signIn",
572
+ args: {
573
+ provider: "passkey",
574
+ params: phase2Params,
575
+ verifier: phase1Result.verifier,
576
+ },
577
+ });
578
+ }
579
+ else {
580
+ phase2Result = await convex.action("auth:signIn", {
581
+ provider: "passkey",
582
+ params: phase2Params,
583
+ verifier: phase1Result.verifier,
584
+ });
585
+ }
586
+ if (phase2Result.tokens) {
587
+ if (proxy) {
588
+ await setToken({
589
+ shouldStore: false,
590
+ tokens: phase2Result.tokens === null
591
+ ? null
592
+ : { token: phase2Result.tokens.token },
593
+ });
594
+ }
595
+ else {
596
+ await setToken({
597
+ shouldStore: true,
598
+ tokens: phase2Result.tokens,
599
+ });
600
+ }
601
+ return { signingIn: true };
602
+ }
603
+ return { signingIn: false };
604
+ },
605
+ /**
606
+ * Authenticate with an existing passkey.
607
+ *
608
+ * Performs the full two-round-trip WebAuthn authentication ceremony:
609
+ * 1. Requests assertion options from the server (challenge, allowed credentials)
610
+ * 2. Calls `navigator.credentials.get()` with the options
611
+ * 3. Sends the assertion back to the server for signature verification
612
+ * 4. Server verifies signature, updates counter, creates session, returns tokens
613
+ *
614
+ * Works in both SPA and proxy (SSR) modes.
615
+ *
616
+ * ```ts
617
+ * // Discoverable credential (no email needed)
618
+ * await auth.passkey.authenticate();
619
+ *
620
+ * // Scoped to a specific user's credentials
621
+ * await auth.passkey.authenticate({ email: "user@example.com" });
622
+ *
623
+ * // Autofill-assisted (conditional UI)
624
+ * await auth.passkey.authenticate({ autofill: true });
625
+ * ```
626
+ *
627
+ * @param opts.email - Scope to credentials for this email's user
628
+ * @param opts.autofill - Use conditional mediation (autofill UI)
629
+ * @returns `{ signingIn: true }` on success
630
+ */
631
+ authenticate: async (opts) => {
632
+ const phase1Params = {
633
+ flow: "auth-options",
634
+ email: opts?.email,
635
+ };
636
+ // Phase 1: Get assertion options from server
637
+ let phase1Result;
638
+ if (proxy) {
639
+ phase1Result = await proxyFetch({
640
+ action: "auth:signIn",
641
+ args: { provider: "passkey", params: phase1Params },
642
+ });
643
+ }
644
+ else {
645
+ phase1Result = await convex.action("auth:signIn", {
646
+ provider: "passkey",
647
+ params: phase1Params,
648
+ });
649
+ }
650
+ if (!phase1Result.options) {
651
+ throw new Error("Server did not return passkey authentication options");
652
+ }
653
+ const options = phase1Result.options;
654
+ // Convert base64url strings to ArrayBuffers for the credential API
655
+ const getOptions = {
656
+ publicKey: {
657
+ challenge: base64urlDecode(options.challenge).buffer,
658
+ timeout: options.timeout,
659
+ rpId: options.rpId,
660
+ userVerification: options.userVerification,
661
+ allowCredentials: (options.allowCredentials ?? []).map((cred) => ({
662
+ type: cred.type ?? "public-key",
663
+ id: base64urlDecode(cred.id).buffer,
664
+ transports: cred.transports,
665
+ })),
666
+ },
667
+ ...(opts?.autofill ? { mediation: "conditional" } : {}),
668
+ };
669
+ // Phase 2: Get credential via browser API
670
+ const credential = (await navigator.credentials.get(getOptions));
671
+ if (!credential) {
672
+ throw new Error("Passkey authentication was cancelled");
673
+ }
674
+ const response = credential.response;
675
+ const phase2Params = {
676
+ flow: "auth-verify",
677
+ credentialId: base64urlEncode(credential.rawId),
678
+ clientDataJSON: base64urlEncode(response.clientDataJSON),
679
+ authenticatorData: base64urlEncode(response.authenticatorData),
680
+ signature: base64urlEncode(response.signature),
681
+ };
682
+ // Phase 3: Send assertion to server for verification
683
+ let phase2Result;
684
+ if (proxy) {
685
+ phase2Result = await proxyFetch({
686
+ action: "auth:signIn",
687
+ args: {
688
+ provider: "passkey",
689
+ params: phase2Params,
690
+ verifier: phase1Result.verifier,
691
+ },
692
+ });
693
+ }
694
+ else {
695
+ phase2Result = await convex.action("auth:signIn", {
696
+ provider: "passkey",
697
+ params: phase2Params,
698
+ verifier: phase1Result.verifier,
699
+ });
700
+ }
701
+ if (phase2Result.tokens) {
702
+ if (proxy) {
703
+ await setToken({
704
+ shouldStore: false,
705
+ tokens: phase2Result.tokens === null
706
+ ? null
707
+ : { token: phase2Result.tokens.token },
708
+ });
709
+ }
710
+ else {
711
+ await setToken({
712
+ shouldStore: true,
713
+ tokens: phase2Result.tokens,
714
+ });
715
+ }
716
+ return { signingIn: true };
717
+ }
718
+ return { signingIn: false };
719
+ },
720
+ };
721
+ const totp = {
722
+ /**
723
+ * Start TOTP enrollment. Must be authenticated.
724
+ *
725
+ * Returns a URI for QR code display and a base32 secret for manual entry.
726
+ *
727
+ * ```ts
728
+ * const setup = await auth.totp.setup();
729
+ * // Display QR code from setup.uri
730
+ * // Or show setup.secret for manual entry
731
+ * ```
732
+ */
733
+ setup: async (opts) => {
734
+ const params = { flow: "setup" };
735
+ if (opts?.name)
736
+ params.name = opts.name;
737
+ if (opts?.accountName)
738
+ params.accountName = opts.accountName;
739
+ if (proxy) {
740
+ const result = await proxyFetch({
741
+ action: "auth:signIn",
742
+ args: { provider: "totp", params },
743
+ });
744
+ return { uri: result.totpSetup.uri, secret: result.totpSetup.secret, verifier: result.verifier, totpId: result.totpSetup.totpId };
745
+ }
746
+ const result = await convex.action("auth:signIn", {
747
+ provider: "totp",
748
+ params,
749
+ });
750
+ return { uri: result.totpSetup.uri, secret: result.totpSetup.secret, verifier: result.verifier, totpId: result.totpSetup.totpId };
751
+ },
752
+ /**
753
+ * Complete TOTP enrollment by verifying the first code from the authenticator app.
754
+ *
755
+ * ```ts
756
+ * await auth.totp.confirm({ code: "123456", verifier: setup.verifier, totpId: setup.totpId });
757
+ * ```
758
+ */
759
+ confirm: async (opts) => {
760
+ const params = {
761
+ flow: "confirm",
762
+ code: opts.code,
763
+ totpId: opts.totpId,
764
+ };
765
+ if (proxy) {
766
+ const result = await proxyFetch({
767
+ action: "auth:signIn",
768
+ args: { provider: "totp", params, verifier: opts.verifier },
769
+ });
770
+ if (result.tokens) {
771
+ await setToken({
772
+ shouldStore: false,
773
+ tokens: result.tokens === null ? null : { token: result.tokens.token },
774
+ });
775
+ }
776
+ return;
777
+ }
778
+ const result = await convex.action("auth:signIn", {
779
+ provider: "totp",
780
+ params,
781
+ verifier: opts.verifier,
782
+ });
783
+ if (result.tokens) {
784
+ await setToken({
785
+ shouldStore: true,
786
+ tokens: result.tokens ?? null,
787
+ });
788
+ }
789
+ },
790
+ /**
791
+ * Complete 2FA verification during sign-in.
792
+ *
793
+ * Called after a credentials sign-in returns `totpRequired: true`.
794
+ *
795
+ * ```ts
796
+ * const result = await auth.signIn("password", { email, password });
797
+ * if (result.totpRequired) {
798
+ * await auth.totp.verify({ code: "123456", verifier: result.verifier! });
799
+ * }
800
+ * ```
801
+ */
802
+ verify: async (opts) => {
803
+ const params = {
804
+ flow: "verify",
805
+ code: opts.code,
806
+ };
807
+ if (proxy) {
808
+ const result = await proxyFetch({
809
+ action: "auth:signIn",
810
+ args: { provider: "totp", params, verifier: opts.verifier },
811
+ });
812
+ if (result.tokens) {
813
+ await setToken({
814
+ shouldStore: false,
815
+ tokens: result.tokens === null ? null : { token: result.tokens.token },
816
+ });
817
+ }
818
+ return;
819
+ }
820
+ const result = await convex.action("auth:signIn", {
821
+ provider: "totp",
822
+ params,
823
+ verifier: opts.verifier,
824
+ });
825
+ if (result.tokens) {
826
+ await setToken({
827
+ shouldStore: true,
828
+ tokens: result.tokens ?? null,
829
+ });
830
+ }
831
+ },
832
+ };
414
833
  return {
415
834
  /** Current auth state snapshot. */
416
835
  get state() {
@@ -419,6 +838,10 @@ export function client(options) {
419
838
  signIn,
420
839
  signOut,
421
840
  onChange,
841
+ /** Passkey (WebAuthn) authentication helpers. */
842
+ passkey,
843
+ /** TOTP two-factor authentication helpers. */
844
+ totp,
422
845
  };
423
846
  }
424
847
  // ---------------------------------------------------------------------------