@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28

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 (88) hide show
  1. package/README.md +3 -5
  2. package/dist/bin.js +6488 -1571
  3. package/dist/browser/index.js +10 -7
  4. package/dist/browser/locks.js +3 -5
  5. package/dist/browser/navigation.js +7 -10
  6. package/dist/browser/runtime.js +35 -33
  7. package/dist/client/core/types.js +17 -0
  8. package/dist/client/factors/device.js +26 -19
  9. package/dist/client/index.js +151 -163
  10. package/dist/client/runtime/proxy.js +6 -6
  11. package/dist/client/services/adapters.js +3 -7
  12. package/dist/client/services/http.js +2 -5
  13. package/dist/client/services/resolve.js +5 -11
  14. package/dist/client/services/runtime.js +2 -5
  15. package/dist/component/_generated/component.d.ts +46 -0
  16. package/dist/component/index.d.ts +3 -3
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/public/identity/sessions.js +38 -1
  19. package/dist/component/public/identity/tokens.js +81 -3
  20. package/dist/component/public/identity/verifiers.js +9 -3
  21. package/dist/component/public.js +3 -3
  22. package/dist/component/schema.d.ts +320 -320
  23. package/dist/core/index.d.ts +380 -0
  24. package/dist/core/index.js +83 -0
  25. package/dist/otel.d.ts +13 -17
  26. package/dist/otel.js +39 -49
  27. package/dist/providers/email.d.ts +2 -2
  28. package/dist/providers/password.js +8 -16
  29. package/dist/providers/phone.js +2 -9
  30. package/dist/server/auth-context.d.ts +204 -0
  31. package/dist/server/auth-context.js +76 -0
  32. package/dist/server/auth.d.ts +25 -187
  33. package/dist/server/auth.js +5 -96
  34. package/dist/server/componentContext.d.ts +12 -0
  35. package/dist/server/componentContext.js +1 -0
  36. package/dist/server/config.js +1 -12
  37. package/dist/server/constants.js +6 -0
  38. package/dist/server/contract.d.ts +1 -1
  39. package/dist/server/core.js +5 -14
  40. package/dist/server/crypto.js +26 -18
  41. package/dist/server/db.js +6 -1
  42. package/dist/server/device.js +88 -78
  43. package/dist/server/http.d.ts +4 -3
  44. package/dist/server/http.js +74 -86
  45. package/dist/server/index.d.ts +2 -1
  46. package/dist/server/limits.js +22 -15
  47. package/dist/server/mounts.d.ts +103 -103
  48. package/dist/server/mutations/account.js +6 -4
  49. package/dist/server/mutations/invalidate.js +3 -6
  50. package/dist/server/mutations/oauth.js +86 -88
  51. package/dist/server/mutations/refresh.js +45 -87
  52. package/dist/server/mutations/register.js +19 -19
  53. package/dist/server/mutations/retrieve.js +17 -15
  54. package/dist/server/mutations/signature.js +9 -13
  55. package/dist/server/mutations/signin.js +7 -3
  56. package/dist/server/mutations/signout.js +10 -15
  57. package/dist/server/mutations/store.js +22 -12
  58. package/dist/server/mutations/verifier.js +11 -6
  59. package/dist/server/mutations/verify.js +55 -46
  60. package/dist/server/oauth/runtime.js +27 -25
  61. package/dist/server/passkey.js +299 -250
  62. package/dist/server/prefetch.js +283 -281
  63. package/dist/server/refresh.js +7 -60
  64. package/dist/server/runtime.d.ts +82 -206
  65. package/dist/server/runtime.js +63 -56
  66. package/dist/server/services/config.js +5 -3
  67. package/dist/server/services/logger.js +2 -4
  68. package/dist/server/services/providers.js +2 -4
  69. package/dist/server/services/refresh.js +2 -4
  70. package/dist/server/services/resolve.js +15 -14
  71. package/dist/server/services/signin.js +2 -4
  72. package/dist/server/sessions.js +32 -33
  73. package/dist/server/signin.js +177 -142
  74. package/dist/server/sso/domain.d.ts +20 -68
  75. package/dist/server/sso/domain.js +444 -413
  76. package/dist/server/sso/http.js +53 -59
  77. package/dist/server/sso/oidc.js +94 -80
  78. package/dist/server/tokens.js +13 -3
  79. package/dist/server/totp.js +153 -116
  80. package/dist/server/types.d.ts +2 -2
  81. package/dist/server/users.js +18 -23
  82. package/dist/server/utils/cache.js +51 -0
  83. package/dist/server/utils/dispatch.js +36 -0
  84. package/dist/server/utils/retry.js +24 -0
  85. package/dist/server/utils/span.js +32 -0
  86. package/dist/shared/errors.js +9 -3
  87. package/dist/shared/log.js +20 -22
  88. package/package.json +41 -33
@@ -1,14 +1,12 @@
1
1
  import { userIdFromIdentitySubject } from "./identity.js";
2
- import { siteUrlsFromEnv } from "./url.js";
3
- import { authFlowError } from "../shared/errors.js";
4
- import { toConvexError } from "./errors.js";
5
2
  import { authDb } from "./db.js";
6
- import { callVerifierSignature } from "./mutations/signature.js";
7
3
  import { callSignIn } from "./mutations/signin.js";
8
4
  import { callVerifier } from "./mutations/verifier.js";
5
+ import { siteUrlsFromEnv } from "./url.js";
6
+ import { authFlowError } from "../shared/errors.js";
7
+ import { toConvexError } from "./errors.js";
9
8
  import { mutatePasskeyInsert, mutatePasskeyUpdateCounter, mutateVerifierDelete, queryPasskeyByCredentialId, queryPasskeysByUserId, queryUserById, queryUserByVerifiedEmail, queryVerifierById } from "./types.js";
10
9
  import { ConvexError } from "convex/values";
11
- import { Effect, Match } from "effect";
12
10
  import { sha256 } from "@oslojs/crypto/sha2";
13
11
  import { decodeBase64urlIgnorePadding, encodeBase64urlNoPadding } from "@oslojs/encoding";
14
12
  import { decodePKIXECDSASignature, decodeSEC1PublicKey, p256, verifyECDSASignature } from "@oslojs/crypto/ecdsa";
@@ -48,167 +46,204 @@ const requireStringParam = (value, name) => {
48
46
  };
49
47
  const convexError = (code, message) => toConvexError(authFlowError(code, message));
50
48
  const asConvexError = (error, code, message) => error instanceof ConvexError ? error : error instanceof Error ? toConvexError(authFlowError(code, error.message || message)) : convexError(code, message);
51
- const resolveRpOptionsFx = (provider) => Effect.sync(() => {
52
- const configuredSiteUrls = process.env.SITE_URL === void 0 ? null : siteUrlsFromEnv();
53
- const siteUrl = configuredSiteUrls?.primaryUrl;
54
- const hasSiteUrl = siteUrl !== void 0 && siteUrl !== "";
55
- const hasRpId = provider.options.rpId !== void 0;
56
- if (!hasSiteUrl && !hasRpId) throw convexError("PASSKEY_MISSING_CONFIG", "Passkey provider requires SITE_URL env var (your frontend URL) or explicit rpId / origin in the provider config. CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.");
57
- const siteHostname = siteUrl ? new URL(siteUrl).hostname : void 0;
58
- const defaultOrigin = configuredSiteUrls?.allowedUrls ?? siteUrl;
59
- return {
60
- rpName: provider.options.rpName ?? siteHostname ?? "localhost",
61
- rpId: provider.options.rpId ?? siteHostname ?? "localhost",
62
- origin: provider.options.origin ?? defaultOrigin ?? "http://localhost",
63
- attestation: provider.options.attestation ?? "none",
64
- userVerification: provider.options.userVerification ?? "required",
65
- residentKey: provider.options.residentKey ?? "preferred",
66
- authenticatorAttachment: provider.options.authenticatorAttachment,
67
- algorithms: provider.options.algorithms ?? [coseAlgorithmES256, coseAlgorithmRS256],
68
- challengeExpirationMs: provider.options.challengeExpirationMs ?? 3e5
69
- };
70
- }).pipe(Effect.catch((error) => Effect.fail(asConvexError(error, "PASSKEY_MISSING_CONFIG", "Passkey relying party configuration is invalid."))));
71
- const verifyClientDataType = (expectedType, label) => (clientData) => clientData.type === expectedType ? Effect.succeed(clientData) : Effect.fail(convexError("PASSKEY_INVALID_CLIENT_DATA", `Invalid client data type: expected ${label}`));
72
- const verifyOrigin = (rp) => (clientData) => {
49
+ function resolveRpOptions(provider) {
50
+ try {
51
+ const configuredSiteUrls = process.env.SITE_URL === void 0 ? null : siteUrlsFromEnv();
52
+ const siteUrl = configuredSiteUrls?.primaryUrl;
53
+ const hasSiteUrl = siteUrl !== void 0 && siteUrl !== "";
54
+ const hasRpId = provider.options.rpId !== void 0;
55
+ if (!hasSiteUrl && !hasRpId) throw convexError("PASSKEY_MISSING_CONFIG", "Passkey provider requires SITE_URL env var (your frontend URL) or explicit rpId / origin in the provider config. CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.");
56
+ const siteHostname = siteUrl ? new URL(siteUrl).hostname : void 0;
57
+ const defaultOrigin = configuredSiteUrls?.allowedUrls ?? siteUrl;
58
+ return {
59
+ rpName: provider.options.rpName ?? siteHostname ?? "localhost",
60
+ rpId: provider.options.rpId ?? siteHostname ?? "localhost",
61
+ origin: provider.options.origin ?? defaultOrigin ?? "http://localhost",
62
+ attestation: provider.options.attestation ?? "none",
63
+ userVerification: provider.options.userVerification ?? "required",
64
+ residentKey: provider.options.residentKey ?? "preferred",
65
+ authenticatorAttachment: provider.options.authenticatorAttachment,
66
+ algorithms: provider.options.algorithms ?? [coseAlgorithmES256, coseAlgorithmRS256],
67
+ challengeExpirationMs: provider.options.challengeExpirationMs ?? 3e5
68
+ };
69
+ } catch (error) {
70
+ throw asConvexError(error, "PASSKEY_MISSING_CONFIG", "Passkey relying party configuration is invalid.");
71
+ }
72
+ }
73
+ function verifyClientDataType(clientData, expectedType, label) {
74
+ if (clientData.type !== expectedType) throw convexError("PASSKEY_INVALID_CLIENT_DATA", `Invalid client data type: expected ${label}`);
75
+ return clientData;
76
+ }
77
+ function verifyOrigin(clientData, rp) {
73
78
  const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
74
- return allowed.includes(clientData.origin) ? Effect.succeed(clientData) : Effect.fail(convexError("PASSKEY_INVALID_ORIGIN", `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`));
75
- };
76
- const verifyAndConsumeChallenge = (ctx, verifierValue) => (clientData) => {
79
+ if (!allowed.includes(clientData.origin)) throw convexError("PASSKEY_INVALID_ORIGIN", `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`);
80
+ return clientData;
81
+ }
82
+ async function verifyAndConsumeChallenge(clientData, ctx, verifierValue) {
77
83
  const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(clientData.challenge)));
78
- return Effect.gen(function* () {
79
- const doc = yield* Effect.tryPromise({
80
- try: () => queryVerifierById(ctx, verifierValue),
81
- catch: () => convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.")
82
- });
83
- if (!doc || doc.signature !== challengeHash) return yield* Effect.fail(convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge."));
84
- yield* Effect.tryPromise({
85
- try: () => mutateVerifierDelete(ctx, verifierValue),
86
- catch: () => convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.")
87
- });
88
- return clientData;
89
- });
90
- };
91
- const verifyRpId = (rpId) => (authData) => authData.verifyRelyingPartyIdHash(rpId) ? Effect.succeed(authData) : Effect.fail(convexError("PASSKEY_RP_MISMATCH", "Relying party ID mismatch."));
92
- const verifyUserFlags = (rp) => (authData) => !authData.userPresent ? Effect.fail(convexError("PASSKEY_USER_PRESENCE", "User presence flag not set.")) : rp.userVerification === "required" && !authData.userVerified ? Effect.fail(convexError("PASSKEY_USER_VERIFICATION", "User verification required but not performed.")) : Effect.succeed(authData);
93
- const resolvePasskeyDispatchFx = (params) => {
84
+ let doc;
85
+ try {
86
+ doc = await queryVerifierById(ctx, verifierValue);
87
+ } catch {
88
+ throw convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.");
89
+ }
90
+ if (!doc || doc.signature !== challengeHash) throw convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.");
91
+ try {
92
+ await mutateVerifierDelete(ctx, verifierValue);
93
+ } catch {
94
+ throw convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.");
95
+ }
96
+ return clientData;
97
+ }
98
+ function verifyRpId(authData, rpId) {
99
+ if (!authData.verifyRelyingPartyIdHash(rpId)) throw convexError("PASSKEY_RP_MISMATCH", "Relying party ID mismatch.");
100
+ return authData;
101
+ }
102
+ function verifyUserFlags(authData, rp) {
103
+ if (!authData.userPresent) throw convexError("PASSKEY_USER_PRESENCE", "User presence flag not set.");
104
+ if (rp.userVerification === "required" && !authData.userVerified) throw convexError("PASSKEY_USER_VERIFICATION", "User verification required but not performed.");
105
+ return authData;
106
+ }
107
+ function resolvePasskeyDispatch(params) {
94
108
  const flow = params.flow;
95
- return typeof flow === "string" && PASSKEY_FLOWS.includes(flow) ? Effect.succeed({ flow }) : Effect.fail(convexError("PASSKEY_MISSING_FLOW", "Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify"));
96
- };
97
- const requirePasskeyVerifierFx = (verifier) => verifier != null ? Effect.succeed(verifier) : Effect.fail(convexError("PASSKEY_MISSING_VERIFIER", "Missing verifier for passkey operation."));
98
- const requireAuthenticatedUserId = (ctx) => Effect.flatMap(Effect.tryPromise({
99
- try: () => ctx.auth.getUserIdentity(),
100
- catch: () => convexError("PASSKEY_AUTH_REQUIRED", "Sign in first, then add a passkey to your account.")
101
- }), (identity) => Match.value(identity).pipe(Match.when(null, () => Effect.fail(convexError("PASSKEY_AUTH_REQUIRED", "Sign in first, then add a passkey to your account."))), Match.orElse((identity$1) => Effect.succeed(userIdFromIdentitySubject(identity$1.subject)))));
102
- const resolveRegistrationPublicKeyBytes = (publicKey, algorithm) => Match.value(algorithm).pipe(Match.when(coseAlgorithmES256, () => {
103
- const ec2 = publicKey.ec2();
104
- const xBytes = new Uint8Array(32);
105
- let vx = ec2.x;
106
- for (let i = 31; i >= 0; i--) {
107
- xBytes[i] = Number(vx & 255n);
108
- vx >>= 8n;
109
+ if (typeof flow === "string" && PASSKEY_FLOWS.includes(flow)) return { flow };
110
+ throw convexError("PASSKEY_MISSING_FLOW", "Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify");
111
+ }
112
+ function requirePasskeyVerifier(verifier) {
113
+ if (verifier != null) return verifier;
114
+ throw convexError("PASSKEY_MISSING_VERIFIER", "Missing verifier for passkey operation.");
115
+ }
116
+ async function requireAuthenticatedUserId(ctx) {
117
+ let identity;
118
+ try {
119
+ identity = await ctx.auth.getUserIdentity();
120
+ } catch {
121
+ throw convexError("PASSKEY_AUTH_REQUIRED", "Sign in first, then add a passkey to your account.");
109
122
  }
110
- const yBytes = new Uint8Array(32);
111
- let vy = ec2.y;
112
- for (let i = 31; i >= 0; i--) {
113
- yBytes[i] = Number(vy & 255n);
114
- vy >>= 8n;
123
+ if (identity === null) throw convexError("PASSKEY_AUTH_REQUIRED", "Sign in first, then add a passkey to your account.");
124
+ return userIdFromIdentitySubject(identity.subject);
125
+ }
126
+ function resolveRegistrationPublicKeyBytes(publicKey, algorithm) {
127
+ if (algorithm === coseAlgorithmES256) {
128
+ const ec2 = publicKey.ec2();
129
+ const xBytes = new Uint8Array(32);
130
+ let vx = ec2.x;
131
+ for (let i = 31; i >= 0; i--) {
132
+ xBytes[i] = Number(vx & 255n);
133
+ vx >>= 8n;
134
+ }
135
+ const yBytes = new Uint8Array(32);
136
+ let vy = ec2.y;
137
+ for (let i = 31; i >= 0; i--) {
138
+ yBytes[i] = Number(vy & 255n);
139
+ vy >>= 8n;
140
+ }
141
+ const bytes = new Uint8Array(65);
142
+ bytes[0] = 4;
143
+ bytes.set(xBytes, 1);
144
+ bytes.set(yBytes, 33);
145
+ return bytes;
115
146
  }
116
- const bytes = new Uint8Array(65);
117
- bytes[0] = 4;
118
- bytes.set(xBytes, 1);
119
- bytes.set(yBytes, 33);
120
- return Effect.succeed(bytes);
121
- }), Match.when(coseAlgorithmRS256, () => {
122
- const rsa = publicKey.rsa();
123
- const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);
124
- return Effect.succeed(rsaPubKey.encodePKCS1());
125
- }), Match.orElse((algorithm$1) => Effect.fail(convexError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${algorithm$1}`))));
126
- const verifyAssertionSignature = (passkey, signature, messageHash) => Match.value(passkey.algorithm).pipe(Match.when(coseAlgorithmES256, () => {
127
- return verifyECDSASignature(decodeSEC1PublicKey(p256, new Uint8Array(passkey.publicKey)), messageHash, decodePKIXECDSASignature(signature)) ? Effect.void : Effect.fail(convexError("PASSKEY_INVALID_SIGNATURE", "Invalid passkey signature."));
128
- }), Match.when(coseAlgorithmRS256, () => {
129
- return verifyRSASSAPKCS1v15Signature(decodePKCS1RSAPublicKey(new Uint8Array(passkey.publicKey)), sha256ObjectIdentifier, messageHash, signature) ? Effect.void : Effect.fail(convexError("PASSKEY_INVALID_SIGNATURE", "Invalid passkey signature."));
130
- }), Match.orElse((algorithm) => Effect.fail(convexError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${algorithm}`))));
131
- function handlePasskeyFx(ctx, provider, args) {
147
+ if (algorithm === coseAlgorithmRS256) {
148
+ const rsa = publicKey.rsa();
149
+ return new RSAPublicKey(rsa.n, rsa.e).encodePKCS1();
150
+ }
151
+ throw convexError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${algorithm}`);
152
+ }
153
+ function verifyAssertionSignature(passkey, signature, messageHash) {
154
+ if (passkey.algorithm === coseAlgorithmES256) {
155
+ if (!verifyECDSASignature(decodeSEC1PublicKey(p256, new Uint8Array(passkey.publicKey)), messageHash, decodePKIXECDSASignature(signature))) throw convexError("PASSKEY_INVALID_SIGNATURE", "Invalid passkey signature.");
156
+ return;
157
+ }
158
+ if (passkey.algorithm === coseAlgorithmRS256) {
159
+ if (!verifyRSASSAPKCS1v15Signature(decodePKCS1RSAPublicKey(new Uint8Array(passkey.publicKey)), sha256ObjectIdentifier, messageHash, signature)) throw convexError("PASSKEY_INVALID_SIGNATURE", "Invalid passkey signature.");
160
+ return;
161
+ }
162
+ throw convexError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${passkey.algorithm}`);
163
+ }
164
+ async function handlePasskeyFx(ctx, provider, args) {
132
165
  const params = args.params ?? {};
133
- return Effect.flatMap(resolvePasskeyDispatchFx(params), (dispatch) => Match.value(dispatch).pipe(Match.when({ flow: "registerOptions" }, () => Effect.gen(function* () {
134
- const userId = yield* requireAuthenticatedUserId(ctx);
135
- const rp = yield* resolveRpOptionsFx(provider);
136
- const challenge = new Uint8Array(32);
137
- crypto.getRandomValues(challenge);
138
- const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
139
- const verifier = yield* Effect.tryPromise({
140
- try: async () => {
141
- const verifier$1 = await callVerifier(ctx);
142
- await callVerifierSignature(ctx, {
143
- verifier: verifier$1,
144
- signature: challengeHash
145
- });
146
- return verifier$1;
147
- },
148
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
149
- });
150
- const user = yield* Effect.tryPromise({
151
- try: () => queryUserById(ctx, userId),
152
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
153
- });
154
- const userName = params.userName ?? user?.email ?? "user";
155
- const userDisplayName = params.userDisplayName ?? user?.name ?? userName;
156
- const excludeCredentials = (yield* Effect.tryPromise({
157
- try: () => queryPasskeysByUserId(ctx, userId),
158
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
159
- })).map((pk) => ({
160
- id: pk.credentialId,
161
- transports: pk.transports
162
- }));
163
- const userHandle = encodeBase64urlNoPadding(new TextEncoder().encode(userId));
164
- return {
165
- kind: "passkeyOptions",
166
- options: {
167
- rp: {
168
- name: rp.rpName,
169
- id: rp.rpId
170
- },
171
- user: {
172
- id: userHandle,
173
- name: userName,
174
- displayName: userDisplayName
175
- },
176
- challenge: encodeBase64urlNoPadding(challenge),
177
- pubKeyCredParams: rp.algorithms.map((alg) => ({
178
- type: "public-key",
179
- alg
180
- })),
181
- timeout: rp.challengeExpirationMs,
182
- attestation: rp.attestation,
183
- authenticatorSelection: {
184
- residentKey: rp.residentKey,
185
- requireResidentKey: rp.residentKey === "required",
186
- userVerification: rp.userVerification,
187
- ...rp.authenticatorAttachment ? { authenticatorAttachment: rp.authenticatorAttachment } : {}
166
+ const dispatch = resolvePasskeyDispatch(params);
167
+ const handler = {
168
+ registerOptions: async () => {
169
+ const userId = await requireAuthenticatedUserId(ctx);
170
+ const rp = resolveRpOptions(provider);
171
+ const challenge = new Uint8Array(32);
172
+ crypto.getRandomValues(challenge);
173
+ const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
174
+ let verifier;
175
+ try {
176
+ verifier = await callVerifier(ctx, challengeHash);
177
+ } catch {
178
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
179
+ }
180
+ let user;
181
+ try {
182
+ user = await queryUserById(ctx, userId);
183
+ } catch {
184
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
185
+ }
186
+ const userName = params.userName ?? user?.email ?? "user";
187
+ const userDisplayName = params.userDisplayName ?? user?.name ?? userName;
188
+ let existing;
189
+ try {
190
+ existing = await queryPasskeysByUserId(ctx, userId);
191
+ } catch {
192
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
193
+ }
194
+ const excludeCredentials = existing.map((pk) => ({
195
+ id: pk.credentialId,
196
+ transports: pk.transports
197
+ }));
198
+ const userHandle = encodeBase64urlNoPadding(new TextEncoder().encode(userId));
199
+ return {
200
+ kind: "passkeyOptions",
201
+ options: {
202
+ rp: {
203
+ name: rp.rpName,
204
+ id: rp.rpId
205
+ },
206
+ user: {
207
+ id: userHandle,
208
+ name: userName,
209
+ displayName: userDisplayName
210
+ },
211
+ challenge: encodeBase64urlNoPadding(challenge),
212
+ pubKeyCredParams: rp.algorithms.map((alg) => ({
213
+ type: "public-key",
214
+ alg
215
+ })),
216
+ timeout: rp.challengeExpirationMs,
217
+ attestation: rp.attestation,
218
+ authenticatorSelection: {
219
+ residentKey: rp.residentKey,
220
+ requireResidentKey: rp.residentKey === "required",
221
+ userVerification: rp.userVerification,
222
+ ...rp.authenticatorAttachment ? { authenticatorAttachment: rp.authenticatorAttachment } : {}
223
+ },
224
+ excludeCredentials
188
225
  },
189
- excludeCredentials
190
- },
191
- verifier
192
- };
193
- })), Match.when({ flow: "registerVerify" }, () => Effect.gen(function* () {
194
- const userId = yield* requireAuthenticatedUserId(ctx);
195
- const rp = yield* resolveRpOptionsFx(provider);
196
- const verifier = yield* requirePasskeyVerifierFx(args.verifier);
197
- const clientData = parseClientDataJSON(decodeBase64urlIgnorePadding(requireStringParam(params.clientDataJSON, "clientDataJSON")));
198
- yield* verifyClientDataType(ClientDataType.Create, "webauthn.create")(clientData);
199
- yield* verifyOrigin(rp)(clientData);
200
- yield* verifyAndConsumeChallenge(ctx, verifier)(clientData);
201
- const authData = parseAttestationObject(decodeBase64urlIgnorePadding(requireStringParam(params.attestationObject, "attestationObject"))).authenticatorData;
202
- yield* verifyRpId(rp.rpId)(authData);
203
- yield* verifyUserFlags(rp)(authData);
204
- if (authData.credential == null) return yield* Effect.fail(convexError("PASSKEY_NO_CREDENTIAL", "No credential in attestation."));
205
- const credential = authData.credential;
206
- const credentialId = encodeBase64urlNoPadding(credential.id);
207
- const publicKey = credential.publicKey;
208
- const algorithm = publicKey.isAlgorithmDefined() ? publicKey.algorithm() : Match.value(publicKey.type()).pipe(Match.when(COSEKeyType.EC2, () => coseAlgorithmES256), Match.when(COSEKeyType.RSA, () => coseAlgorithmRS256), Match.orElse(() => coseAlgorithmES256));
209
- const publicKeyBytes = yield* resolveRegistrationPublicKeyBytes(publicKey, algorithm);
210
- yield* Effect.tryPromise({
211
- try: async () => {
226
+ verifier
227
+ };
228
+ },
229
+ registerVerify: async () => {
230
+ const userId = await requireAuthenticatedUserId(ctx);
231
+ const rp = resolveRpOptions(provider);
232
+ const verifier = requirePasskeyVerifier(args.verifier);
233
+ const clientData = parseClientDataJSON(decodeBase64urlIgnorePadding(requireStringParam(params.clientDataJSON, "clientDataJSON")));
234
+ verifyClientDataType(clientData, ClientDataType.Create, "webauthn.create");
235
+ verifyOrigin(clientData, rp);
236
+ await verifyAndConsumeChallenge(clientData, ctx, verifier);
237
+ const authData = parseAttestationObject(decodeBase64urlIgnorePadding(requireStringParam(params.attestationObject, "attestationObject"))).authenticatorData;
238
+ verifyRpId(authData, rp.rpId);
239
+ verifyUserFlags(authData, rp);
240
+ if (authData.credential == null) throw convexError("PASSKEY_NO_CREDENTIAL", "No credential in attestation.");
241
+ const credential = authData.credential;
242
+ const credentialId = encodeBase64urlNoPadding(credential.id);
243
+ const publicKey = credential.publicKey;
244
+ const algorithm = publicKey.isAlgorithmDefined() ? publicKey.algorithm() : publicKey.type() === COSEKeyType.EC2 ? coseAlgorithmES256 : publicKey.type() === COSEKeyType.RSA ? coseAlgorithmRS256 : coseAlgorithmES256;
245
+ const publicKeyBytes = resolveRegistrationPublicKeyBytes(publicKey, algorithm);
246
+ try {
212
247
  const deviceType = params.deviceType ?? "single-device";
213
248
  const backedUp = params.backedUp ?? false;
214
249
  await authDb(ctx, ctx.auth.config).accounts.create({
@@ -228,103 +263,117 @@ function handlePasskeyFx(ctx, provider, args) {
228
263
  name: params.passkeyName,
229
264
  createdAt: Date.now()
230
265
  });
231
- },
232
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
233
- });
234
- return {
235
- kind: "signedIn",
236
- signedIn: yield* Effect.tryPromise({
237
- try: () => callSignIn(ctx, {
266
+ } catch {
267
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
268
+ }
269
+ let signInResult;
270
+ try {
271
+ signInResult = await callSignIn(ctx, {
238
272
  userId,
239
273
  generateTokens: true
240
- }),
241
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
242
- })
243
- };
244
- })), Match.when({ flow: "authOptions" }, () => Effect.gen(function* () {
245
- const rp = yield* resolveRpOptionsFx(provider);
246
- const challenge = new Uint8Array(32);
247
- crypto.getRandomValues(challenge);
248
- const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
249
- const verifier = yield* Effect.tryPromise({
250
- try: async () => {
251
- const verifier$1 = await callVerifier(ctx);
252
- await callVerifierSignature(ctx, {
253
- verifier: verifier$1,
254
- signature: challengeHash
255
- });
256
- return verifier$1;
257
- },
258
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
259
- });
260
- let allowCredentials;
261
- if (params.email) {
262
- const email = requireStringParam(params.email, "email");
263
- const user = yield* Effect.tryPromise({
264
- try: () => queryUserByVerifiedEmail(ctx, email),
265
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
266
- });
267
- if (user) {
268
- const passkeys = yield* Effect.tryPromise({
269
- try: () => queryPasskeysByUserId(ctx, user._id),
270
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
271
274
  });
272
- if (passkeys.length > 0) allowCredentials = passkeys.map((pk) => ({
273
- type: "public-key",
274
- id: pk.credentialId,
275
- transports: pk.transports
276
- }));
275
+ } catch {
276
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
277
277
  }
278
- }
279
- const options = {
280
- challenge: encodeBase64urlNoPadding(challenge),
281
- timeout: rp.challengeExpirationMs,
282
- rpId: rp.rpId,
283
- userVerification: rp.userVerification
284
- };
285
- if (allowCredentials) options.allowCredentials = allowCredentials;
286
- return {
287
- kind: "passkeyOptions",
288
- options,
289
- verifier
290
- };
291
- })), Match.when({ flow: "authVerify" }, () => Effect.gen(function* () {
292
- const rp = yield* resolveRpOptionsFx(provider);
293
- const verifier = yield* requirePasskeyVerifierFx(args.verifier);
294
- const clientDataJSON = decodeBase64urlIgnorePadding(requireStringParam(params.clientDataJSON, "clientDataJSON"));
295
- const clientData = parseClientDataJSON(clientDataJSON);
296
- yield* verifyClientDataType(ClientDataType.Get, "webauthn.get")(clientData);
297
- yield* verifyOrigin(rp)(clientData);
298
- yield* verifyAndConsumeChallenge(ctx, verifier)(clientData);
299
- const credentialId = params.credentialId;
300
- if (credentialId == null) return yield* Effect.fail(convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Missing credential ID"));
301
- const passkey = yield* Effect.flatMap(Effect.tryPromise({
302
- try: () => queryPasskeyByCredentialId(ctx, credentialId),
303
- catch: () => convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown passkey credential.")
304
- }), (passkey$1) => Match.value(passkey$1).pipe(Match.when(null, () => Effect.fail(convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown credential"))), Match.orElse((passkey$2) => Effect.succeed(passkey$2))));
305
- const authenticatorDataBytes = decodeBase64urlIgnorePadding(requireStringParam(params.authenticatorData, "authenticatorData"));
306
- const authenticatorData = parseAuthenticatorData(authenticatorDataBytes);
307
- const signature = decodeBase64urlIgnorePadding(requireStringParam(params.signature, "signature"));
308
- const messageHash = sha256(createAssertionSignatureMessage(authenticatorDataBytes, clientDataJSON));
309
- yield* verifyRpId(rp.rpId)(authenticatorData);
310
- yield* verifyUserFlags(rp)(authenticatorData);
311
- yield* verifyAssertionSignature(passkey, signature, messageHash);
312
- if (passkey.counter !== 0 && authenticatorData.signatureCounter !== 0 && authenticatorData.signatureCounter <= passkey.counter) return yield* Effect.fail(convexError("PASSKEY_COUNTER_ERROR", "Authenticator counter did not increase — possible credential cloning detected."));
313
- yield* Effect.tryPromise({
314
- try: () => mutatePasskeyUpdateCounter(ctx, passkey._id, authenticatorData.signatureCounter, Date.now()),
315
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
316
- });
317
- return {
318
- kind: "signedIn",
319
- signedIn: yield* Effect.tryPromise({
320
- try: () => callSignIn(ctx, {
278
+ return {
279
+ kind: "signedIn",
280
+ signedIn: signInResult
281
+ };
282
+ },
283
+ authOptions: async () => {
284
+ const rp = resolveRpOptions(provider);
285
+ const challenge = new Uint8Array(32);
286
+ crypto.getRandomValues(challenge);
287
+ const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
288
+ let verifier;
289
+ try {
290
+ verifier = await callVerifier(ctx, challengeHash);
291
+ } catch {
292
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
293
+ }
294
+ let allowCredentials;
295
+ if (params.email) {
296
+ const email = requireStringParam(params.email, "email");
297
+ let user;
298
+ try {
299
+ user = await queryUserByVerifiedEmail(ctx, email);
300
+ } catch {
301
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
302
+ }
303
+ if (user) {
304
+ let passkeys;
305
+ try {
306
+ passkeys = await queryPasskeysByUserId(ctx, user._id);
307
+ } catch {
308
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
309
+ }
310
+ if (passkeys.length > 0) allowCredentials = passkeys.map((pk) => ({
311
+ type: "public-key",
312
+ id: pk.credentialId,
313
+ transports: pk.transports
314
+ }));
315
+ }
316
+ }
317
+ const options = {
318
+ challenge: encodeBase64urlNoPadding(challenge),
319
+ timeout: rp.challengeExpirationMs,
320
+ rpId: rp.rpId,
321
+ userVerification: rp.userVerification
322
+ };
323
+ if (allowCredentials) options.allowCredentials = allowCredentials;
324
+ return {
325
+ kind: "passkeyOptions",
326
+ options,
327
+ verifier
328
+ };
329
+ },
330
+ authVerify: async () => {
331
+ const rp = resolveRpOptions(provider);
332
+ const verifier = requirePasskeyVerifier(args.verifier);
333
+ const clientDataJSON = decodeBase64urlIgnorePadding(requireStringParam(params.clientDataJSON, "clientDataJSON"));
334
+ const clientData = parseClientDataJSON(clientDataJSON);
335
+ verifyClientDataType(clientData, ClientDataType.Get, "webauthn.get");
336
+ verifyOrigin(clientData, rp);
337
+ await verifyAndConsumeChallenge(clientData, ctx, verifier);
338
+ const credentialId = params.credentialId;
339
+ if (credentialId == null) throw convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Missing credential ID");
340
+ let passkey;
341
+ try {
342
+ passkey = await queryPasskeyByCredentialId(ctx, credentialId);
343
+ } catch {
344
+ throw convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown passkey credential.");
345
+ }
346
+ if (passkey === null) throw convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown credential");
347
+ const authenticatorDataBytes = decodeBase64urlIgnorePadding(requireStringParam(params.authenticatorData, "authenticatorData"));
348
+ const authenticatorData = parseAuthenticatorData(authenticatorDataBytes);
349
+ const signatureBytes = decodeBase64urlIgnorePadding(requireStringParam(params.signature, "signature"));
350
+ const messageHash = sha256(createAssertionSignatureMessage(authenticatorDataBytes, clientDataJSON));
351
+ verifyRpId(authenticatorData, rp.rpId);
352
+ verifyUserFlags(authenticatorData, rp);
353
+ verifyAssertionSignature(passkey, signatureBytes, messageHash);
354
+ if (passkey.counter !== 0 && authenticatorData.signatureCounter !== 0 && authenticatorData.signatureCounter <= passkey.counter) throw convexError("PASSKEY_COUNTER_ERROR", "Authenticator counter did not increase — possible credential cloning detected.");
355
+ try {
356
+ await mutatePasskeyUpdateCounter(ctx, passkey._id, authenticatorData.signatureCounter, Date.now());
357
+ } catch {
358
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
359
+ }
360
+ let signInResult;
361
+ try {
362
+ signInResult = await callSignIn(ctx, {
321
363
  userId: passkey.userId,
322
364
  generateTokens: true
323
- }),
324
- catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
325
- })
326
- };
327
- })), Match.exhaustive));
365
+ });
366
+ } catch {
367
+ throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
368
+ }
369
+ return {
370
+ kind: "signedIn",
371
+ signedIn: signInResult
372
+ };
373
+ }
374
+ }[dispatch.flow];
375
+ if (!handler) throw convexError("PASSKEY_MISSING_FLOW", `Unknown passkey flow: ${dispatch.flow}`);
376
+ return handler();
328
377
  }
329
378
 
330
379
  //#endregion