@supa-media/convex 1.7.0 → 1.8.0
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.
- package/package.json +1 -1
- package/src/auth/index.ts +1 -0
- package/src/auth/setup.ts +14 -3
- package/src/auth/twilioVerify.ts +15 -5
- package/src/auth/users.ts +20 -2
package/package.json
CHANGED
package/src/auth/index.ts
CHANGED
package/src/auth/setup.ts
CHANGED
|
@@ -28,10 +28,10 @@ import { Email } from "@convex-dev/auth/providers/Email";
|
|
|
28
28
|
import { Phone } from "@convex-dev/auth/providers/Phone";
|
|
29
29
|
import { assertMayReceiveEmailCode, type SupaAuthAdmission } from "./admission";
|
|
30
30
|
import { createTestEmailOtps, type SupaAuthTestEmailConfig } from "./testEmail";
|
|
31
|
-
import { userCallback, type SupaAuthUserCreated } from "./users";
|
|
31
|
+
import { userCallback, type SupaAuthFindUserByEmail, type SupaAuthUserCreated } from "./users";
|
|
32
32
|
|
|
33
33
|
export { NOT_ADMITTED_MESSAGE, type SupaAuthAdmission } from "./admission";
|
|
34
|
-
export type { SupaAuthUserCreated } from "./users";
|
|
34
|
+
export type { SupaAuthFindUserByEmail, SupaAuthUserCreated } from "./users";
|
|
35
35
|
|
|
36
36
|
export {
|
|
37
37
|
createTestEmailOtp,
|
|
@@ -135,6 +135,15 @@ export interface SupaAuthConfig {
|
|
|
135
135
|
* scheduled action, not here.
|
|
136
136
|
*/
|
|
137
137
|
onUserCreated?: SupaAuthUserCreated;
|
|
138
|
+
/**
|
|
139
|
+
* For an app that lets one person sign in with several emails: the user an
|
|
140
|
+
* address belongs to, or `null`. Asked when an email sign-in has no auth
|
|
141
|
+
* account yet, before the `users.email` match, so a sign-in through an
|
|
142
|
+
* attached address reaches its user rather than making a new one. Only
|
|
143
|
+
* return a user the address was confirmed for: whoever holds that mailbox
|
|
144
|
+
* signs in as them.
|
|
145
|
+
*/
|
|
146
|
+
findUserByEmail?: SupaAuthFindUserByEmail;
|
|
138
147
|
/** Resend email OTP configuration. */
|
|
139
148
|
resend?: SupaAuthResendConfig;
|
|
140
149
|
/** Twilio phone OTP configuration. */
|
|
@@ -425,6 +434,8 @@ export function createSupaAuth(config: SupaAuthConfig = {}) {
|
|
|
425
434
|
|
|
426
435
|
return convexAuth({
|
|
427
436
|
providers,
|
|
428
|
-
callbacks: {
|
|
437
|
+
callbacks: {
|
|
438
|
+
createOrUpdateUser: userCallback(config.admission, config.onUserCreated, config.findUserByEmail),
|
|
439
|
+
},
|
|
429
440
|
});
|
|
430
441
|
}
|
package/src/auth/twilioVerify.ts
CHANGED
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
* the code, so the app stores no secret and only records the result.
|
|
9
9
|
*
|
|
10
10
|
* The keys are the provider's: `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`,
|
|
11
|
-
* `TWILIO_VERIFY_SERVICE_SID`.
|
|
12
|
-
* `
|
|
11
|
+
* `TWILIO_VERIFY_SERVICE_SID`. An API key works in place of the auth token:
|
|
12
|
+
* `TWILIO_API_KEY_SID` with `TWILIO_API_KEY_SECRET`, which Twilio recommends
|
|
13
|
+
* because it can be revoked on its own. With no usable pair, `twilioVerifyKeys`
|
|
14
|
+
* answers `null`, and the app decides what that means. It should never mean
|
|
15
|
+
* "verified".
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
export interface TwilioVerifyKeys {
|
|
16
19
|
accountSid: string;
|
|
20
|
+
/** The account's auth token, or the API key's secret when `apiKeySid` is set. */
|
|
17
21
|
authToken: string;
|
|
18
22
|
serviceSid: string;
|
|
23
|
+
/** Set when signing requests with an API key rather than the auth token. */
|
|
24
|
+
apiKeySid?: string;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export type TwilioSendResult =
|
|
@@ -34,15 +40,19 @@ export function twilioVerifyKeys(
|
|
|
34
40
|
const accountSid = env.TWILIO_ACCOUNT_SID?.trim();
|
|
35
41
|
const authToken = env.TWILIO_AUTH_TOKEN?.trim();
|
|
36
42
|
const serviceSid = env.TWILIO_VERIFY_SERVICE_SID?.trim();
|
|
37
|
-
if (!accountSid || !
|
|
38
|
-
return { accountSid, authToken, serviceSid };
|
|
43
|
+
if (!accountSid || !serviceSid) return null;
|
|
44
|
+
if (authToken) return { accountSid, authToken, serviceSid };
|
|
45
|
+
const apiKeySid = env.TWILIO_API_KEY_SID?.trim();
|
|
46
|
+
const apiKeySecret = env.TWILIO_API_KEY_SECRET?.trim();
|
|
47
|
+
if (!apiKeySid || !apiKeySecret) return null;
|
|
48
|
+
return { accountSid, authToken: apiKeySecret, serviceSid, apiKeySid };
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
function request(keys: TwilioVerifyKeys, path: string, body: Record<string, string>, fetchImpl: Fetch) {
|
|
42
52
|
return fetchImpl(`https://verify.twilio.com/v2/Services/${keys.serviceSid}/${path}`, {
|
|
43
53
|
method: "POST",
|
|
44
54
|
headers: {
|
|
45
|
-
Authorization: `Basic ${btoa(`${keys.accountSid}:${keys.authToken}`)}`,
|
|
55
|
+
Authorization: `Basic ${btoa(`${keys.apiKeySid ?? keys.accountSid}:${keys.authToken}`)}`,
|
|
46
56
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
47
57
|
},
|
|
48
58
|
body: new URLSearchParams(body),
|
package/src/auth/users.ts
CHANGED
|
@@ -15,6 +15,12 @@ import type { convexAuth } from "@convex-dev/auth/server";
|
|
|
15
15
|
|
|
16
16
|
import { assertMayCreateUser, type SupaAuthAdmission } from "./admission";
|
|
17
17
|
|
|
18
|
+
/** See `SupaAuthConfig.findUserByEmail`. */
|
|
19
|
+
export type SupaAuthFindUserByEmail = (
|
|
20
|
+
ctx: GenericMutationCtx<AnyDataModel>,
|
|
21
|
+
email: string,
|
|
22
|
+
) => Promise<GenericId<"users"> | null>;
|
|
23
|
+
|
|
18
24
|
/** See `SupaAuthConfig.onUserCreated`. */
|
|
19
25
|
export type SupaAuthUserCreated = (
|
|
20
26
|
ctx: GenericMutationCtx<AnyDataModel>,
|
|
@@ -28,6 +34,7 @@ type Handler = NonNullable<
|
|
|
28
34
|
export function userCallback(
|
|
29
35
|
admission: SupaAuthAdmission | undefined,
|
|
30
36
|
onUserCreated?: SupaAuthUserCreated,
|
|
37
|
+
findUserByEmail?: SupaAuthFindUserByEmail,
|
|
31
38
|
): Handler {
|
|
32
39
|
async function createOrUpdateUser(
|
|
33
40
|
ctx: Parameters<Handler>[0],
|
|
@@ -44,8 +51,12 @@ export function userCallback(
|
|
|
44
51
|
if (type === "email" || type === "verification") {
|
|
45
52
|
updateData.emailVerificationTime = Date.now();
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
// Fill in a missing address, never replace one. A user may sign in
|
|
55
|
+
// through several auth accounts (an app that lets one person keep a
|
|
56
|
+
// work and a home email), and the address on the user row is the one
|
|
57
|
+
// they chose for mail, not whichever they signed in with last.
|
|
58
|
+
if (profile.phone && !existingUser.phone) updateData.phone = profile.phone;
|
|
59
|
+
if (profile.email && !existingUser.email) updateData.email = profile.email;
|
|
49
60
|
if (profile.name) updateData.name = profile.name;
|
|
50
61
|
|
|
51
62
|
if (Object.keys(updateData).length > 0) {
|
|
@@ -74,6 +85,13 @@ export function userCallback(
|
|
|
74
85
|
// New auth account — try to link to existing user by email
|
|
75
86
|
if (type === "email" && typeof profile.email === "string") {
|
|
76
87
|
const email = profile.email;
|
|
88
|
+
// The app's own answer first: an address it has attached to a user as
|
|
89
|
+
// an extra sign-in email is that user, whatever the user row says.
|
|
90
|
+
const attached = findUserByEmail === undefined ? null : await findUserByEmail(ctx, email);
|
|
91
|
+
if (attached !== null) {
|
|
92
|
+
await ctx.db.patch(attached, { emailVerificationTime: Date.now() });
|
|
93
|
+
return attached;
|
|
94
|
+
}
|
|
77
95
|
const existingUser = await ctx.db
|
|
78
96
|
.query("users")
|
|
79
97
|
.filter((q) => q.eq(q.field("email"), email))
|