@robelest/convex-auth 0.0.2-preview.1 → 0.0.2
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/dist/bin.cjs +466 -63
- package/dist/client/index.d.ts +211 -30
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +673 -59
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +56 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +93 -3
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +2 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/index.d.ts +5 -3
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +5 -3
- package/dist/component/index.js.map +1 -1
- package/dist/component/portalBridge.d.ts +80 -0
- package/dist/component/portalBridge.d.ts.map +1 -0
- package/dist/component/portalBridge.js +102 -0
- package/dist/component/portalBridge.js.map +1 -0
- package/dist/component/public.d.ts +193 -9
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +204 -33
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +89 -9
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +68 -7
- package/dist/component/schema.js.map +1 -1
- package/dist/providers/{Anonymous.d.ts → anonymous.d.ts} +8 -8
- package/dist/providers/{Anonymous.d.ts.map → anonymous.d.ts.map} +1 -1
- package/dist/providers/{Anonymous.js → anonymous.js} +9 -10
- package/dist/providers/anonymous.js.map +1 -0
- package/dist/providers/{ConvexCredentials.d.ts → credentials.d.ts} +11 -11
- package/dist/providers/credentials.d.ts.map +1 -0
- package/dist/providers/{ConvexCredentials.js → credentials.js} +8 -8
- package/dist/providers/credentials.js.map +1 -0
- package/dist/providers/{Email.d.ts → email.d.ts} +6 -6
- package/dist/providers/email.d.ts.map +1 -0
- package/dist/providers/{Email.js → email.js} +6 -6
- package/dist/providers/email.js.map +1 -0
- package/dist/providers/passkey.d.ts +20 -0
- package/dist/providers/passkey.d.ts.map +1 -0
- package/dist/providers/passkey.js +32 -0
- package/dist/providers/passkey.js.map +1 -0
- package/dist/providers/{Password.d.ts → password.d.ts} +10 -10
- package/dist/providers/{Password.d.ts.map → password.d.ts.map} +1 -1
- package/dist/providers/{Password.js → password.js} +19 -20
- package/dist/providers/password.js.map +1 -0
- package/dist/providers/{Phone.d.ts → phone.d.ts} +3 -3
- package/dist/providers/{Phone.d.ts.map → phone.d.ts.map} +1 -1
- package/dist/providers/{Phone.js → phone.js} +3 -3
- package/dist/providers/{Phone.js.map → phone.js.map} +1 -1
- package/dist/providers/totp.d.ts +14 -0
- package/dist/providers/totp.d.ts.map +1 -0
- package/dist/providers/totp.js +23 -0
- package/dist/providers/totp.js.map +1 -0
- package/dist/server/convex-auth.d.ts +243 -0
- package/dist/server/convex-auth.d.ts.map +1 -0
- package/dist/server/convex-auth.js +365 -0
- package/dist/server/convex-auth.js.map +1 -0
- package/dist/server/implementation/index.d.ts +153 -166
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +162 -105
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/passkey.d.ts +33 -0
- package/dist/server/implementation/passkey.d.ts.map +1 -0
- package/dist/server/implementation/passkey.js +450 -0
- package/dist/server/implementation/passkey.js.map +1 -0
- package/dist/server/implementation/redirects.d.ts.map +1 -1
- package/dist/server/implementation/redirects.js +4 -9
- package/dist/server/implementation/redirects.js.map +1 -1
- package/dist/server/implementation/sessions.d.ts +2 -20
- package/dist/server/implementation/sessions.d.ts.map +1 -1
- package/dist/server/implementation/sessions.js +2 -20
- package/dist/server/implementation/sessions.js.map +1 -1
- package/dist/server/implementation/signIn.d.ts +13 -0
- package/dist/server/implementation/signIn.d.ts.map +1 -1
- package/dist/server/implementation/signIn.js +26 -1
- package/dist/server/implementation/signIn.js.map +1 -1
- package/dist/server/implementation/totp.d.ts +40 -0
- package/dist/server/implementation/totp.d.ts.map +1 -0
- package/dist/server/implementation/totp.js +211 -0
- package/dist/server/implementation/totp.js.map +1 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +255 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/portal-email.d.ts +19 -0
- package/dist/server/portal-email.d.ts.map +1 -0
- package/dist/server/portal-email.js +89 -0
- package/dist/server/portal-email.js.map +1 -0
- package/dist/server/portal.d.ts +116 -0
- package/dist/server/portal.d.ts.map +1 -0
- package/dist/server/portal.js +294 -0
- package/dist/server/portal.js.map +1 -0
- package/dist/server/provider_utils.d.ts +1 -1
- package/dist/server/provider_utils.d.ts.map +1 -1
- package/dist/server/provider_utils.js +39 -1
- package/dist/server/provider_utils.js.map +1 -1
- package/dist/server/types.d.ts +128 -11
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/cli/index.ts +48 -6
- package/src/cli/portal-link.ts +112 -0
- package/src/cli/portal-upload.ts +411 -0
- package/src/client/index.ts +823 -109
- package/src/component/_generated/api.ts +72 -1
- package/src/component/_generated/component.ts +180 -4
- package/src/component/convex.config.ts +3 -0
- package/src/component/index.ts +5 -10
- package/src/component/portalBridge.ts +116 -0
- package/src/component/public.ts +231 -37
- package/src/component/schema.ts +70 -7
- package/src/providers/{Anonymous.ts → anonymous.ts} +10 -11
- package/src/providers/{ConvexCredentials.ts → credentials.ts} +11 -11
- package/src/providers/{Email.ts → email.ts} +5 -5
- package/src/providers/passkey.ts +35 -0
- package/src/providers/{Password.ts → password.ts} +22 -27
- package/src/providers/{Phone.ts → phone.ts} +2 -2
- package/src/providers/totp.ts +26 -0
- package/src/server/convex-auth.ts +470 -0
- package/src/server/implementation/index.ts +228 -239
- package/src/server/implementation/passkey.ts +650 -0
- package/src/server/implementation/redirects.ts +4 -11
- package/src/server/implementation/sessions.ts +2 -20
- package/src/server/implementation/signIn.ts +39 -1
- package/src/server/implementation/totp.ts +366 -0
- package/src/server/index.ts +373 -0
- package/src/server/portal-email.ts +95 -0
- package/src/server/portal.ts +375 -0
- package/src/server/provider_utils.ts +42 -1
- package/src/server/types.ts +161 -10
- package/dist/providers/Anonymous.js.map +0 -1
- package/dist/providers/ConvexCredentials.d.ts.map +0 -1
- package/dist/providers/ConvexCredentials.js.map +0 -1
- package/dist/providers/Email.d.ts.map +0 -1
- package/dist/providers/Email.js.map +0 -1
- package/dist/providers/Password.js.map +0 -1
- package/providers/Anonymous/package.json +0 -6
- package/providers/ConvexCredentials/package.json +0 -6
- package/providers/Email/package.json +0 -6
- package/providers/Password/package.json +0 -6
- package/providers/Phone/package.json +0 -6
- package/server/package.json +0 -6
package/src/component/public.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { mutation, query } from "./_generated/server";
|
|
|
5
5
|
// Users
|
|
6
6
|
// ============================================================================
|
|
7
7
|
|
|
8
|
+
/** List all users. */
|
|
9
|
+
export const userList = query({
|
|
10
|
+
args: {},
|
|
11
|
+
handler: async (ctx) => {
|
|
12
|
+
return await ctx.db.query("user").collect();
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
8
16
|
/** Retrieve a user by their document ID. */
|
|
9
17
|
export const userGetById = query({
|
|
10
18
|
args: { userId: v.id("user") },
|
|
@@ -79,6 +87,17 @@ export const userPatch = mutation({
|
|
|
79
87
|
// Accounts
|
|
80
88
|
// ============================================================================
|
|
81
89
|
|
|
90
|
+
/** List all accounts for a user. */
|
|
91
|
+
export const accountListByUser = query({
|
|
92
|
+
args: { userId: v.id("user") },
|
|
93
|
+
handler: async (ctx, { userId }) => {
|
|
94
|
+
return await ctx.db
|
|
95
|
+
.query("account")
|
|
96
|
+
.withIndex("userIdAndProvider", (q) => q.eq("userId", userId as any))
|
|
97
|
+
.collect();
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
82
101
|
/** Look up an account by provider and provider-specific account ID. */
|
|
83
102
|
export const accountGet = query({
|
|
84
103
|
args: { provider: v.string(), providerAccountId: v.string() },
|
|
@@ -133,6 +152,14 @@ export const accountDelete = mutation({
|
|
|
133
152
|
// Sessions
|
|
134
153
|
// ============================================================================
|
|
135
154
|
|
|
155
|
+
/** List all sessions. */
|
|
156
|
+
export const sessionList = query({
|
|
157
|
+
args: {},
|
|
158
|
+
handler: async (ctx) => {
|
|
159
|
+
return await ctx.db.query("session").collect();
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
136
163
|
/** Create a new session for a user with an expiration time. */
|
|
137
164
|
export const sessionCreate = mutation({
|
|
138
165
|
args: { userId: v.id("user"), expirationTime: v.number() },
|
|
@@ -360,6 +387,150 @@ export const refreshTokenGetActive = query({
|
|
|
360
387
|
},
|
|
361
388
|
});
|
|
362
389
|
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// Passkeys
|
|
392
|
+
// ============================================================================
|
|
393
|
+
|
|
394
|
+
/** Store a new passkey credential for a user. */
|
|
395
|
+
export const passkeyInsert = mutation({
|
|
396
|
+
args: {
|
|
397
|
+
userId: v.id("user"),
|
|
398
|
+
credentialId: v.string(),
|
|
399
|
+
publicKey: v.bytes(),
|
|
400
|
+
algorithm: v.number(),
|
|
401
|
+
counter: v.number(),
|
|
402
|
+
transports: v.optional(v.array(v.string())),
|
|
403
|
+
deviceType: v.string(),
|
|
404
|
+
backedUp: v.boolean(),
|
|
405
|
+
name: v.optional(v.string()),
|
|
406
|
+
createdAt: v.number(),
|
|
407
|
+
},
|
|
408
|
+
handler: async (ctx, args) => {
|
|
409
|
+
return await ctx.db.insert("passkey", args);
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
/** Look up a passkey by its credential ID. */
|
|
414
|
+
export const passkeyGetByCredentialId = query({
|
|
415
|
+
args: { credentialId: v.string() },
|
|
416
|
+
handler: async (ctx, { credentialId }) => {
|
|
417
|
+
return await ctx.db
|
|
418
|
+
.query("passkey")
|
|
419
|
+
.withIndex("credentialId", (q) => q.eq("credentialId", credentialId))
|
|
420
|
+
.unique();
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
/** List all passkeys for a user. */
|
|
425
|
+
export const passkeyListByUserId = query({
|
|
426
|
+
args: { userId: v.id("user") },
|
|
427
|
+
handler: async (ctx, { userId }) => {
|
|
428
|
+
return await ctx.db
|
|
429
|
+
.query("passkey")
|
|
430
|
+
.withIndex("userId", (q) => q.eq("userId", userId))
|
|
431
|
+
.collect();
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
/** Update a passkey's counter and last used timestamp after authentication. */
|
|
436
|
+
export const passkeyUpdateCounter = mutation({
|
|
437
|
+
args: { passkeyId: v.id("passkey"), counter: v.number(), lastUsedAt: v.number() },
|
|
438
|
+
handler: async (ctx, { passkeyId, counter, lastUsedAt }) => {
|
|
439
|
+
await ctx.db.patch(passkeyId, { counter, lastUsedAt });
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
/** Update a passkey's metadata (name). */
|
|
444
|
+
export const passkeyUpdateMeta = mutation({
|
|
445
|
+
args: { passkeyId: v.id("passkey"), data: v.any() },
|
|
446
|
+
handler: async (ctx, { passkeyId, data }) => {
|
|
447
|
+
await ctx.db.patch(passkeyId, data);
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
/** Delete a passkey credential. */
|
|
452
|
+
export const passkeyDelete = mutation({
|
|
453
|
+
args: { passkeyId: v.id("passkey") },
|
|
454
|
+
handler: async (ctx, { passkeyId }) => {
|
|
455
|
+
await ctx.db.delete(passkeyId);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// TOTP Two-Factor Authentication
|
|
461
|
+
// ============================================================================
|
|
462
|
+
|
|
463
|
+
/** Store a new TOTP enrollment for a user. */
|
|
464
|
+
export const totpInsert = mutation({
|
|
465
|
+
args: {
|
|
466
|
+
userId: v.id("user"),
|
|
467
|
+
secret: v.bytes(),
|
|
468
|
+
digits: v.number(),
|
|
469
|
+
period: v.number(),
|
|
470
|
+
verified: v.boolean(),
|
|
471
|
+
name: v.optional(v.string()),
|
|
472
|
+
createdAt: v.number(),
|
|
473
|
+
},
|
|
474
|
+
handler: async (ctx, args) => {
|
|
475
|
+
return await ctx.db.insert("totp", args);
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
/** Get a verified TOTP enrollment for a user (returns first match). */
|
|
480
|
+
export const totpGetVerifiedByUserId = query({
|
|
481
|
+
args: { userId: v.id("user") },
|
|
482
|
+
handler: async (ctx, { userId }) => {
|
|
483
|
+
return await ctx.db
|
|
484
|
+
.query("totp")
|
|
485
|
+
.withIndex("userId", (q) => q.eq("userId", userId))
|
|
486
|
+
.filter((q) => q.eq(q.field("verified"), true))
|
|
487
|
+
.first();
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
/** List all TOTP enrollments for a user. */
|
|
492
|
+
export const totpListByUserId = query({
|
|
493
|
+
args: { userId: v.id("user") },
|
|
494
|
+
handler: async (ctx, { userId }) => {
|
|
495
|
+
return await ctx.db
|
|
496
|
+
.query("totp")
|
|
497
|
+
.withIndex("userId", (q) => q.eq("userId", userId))
|
|
498
|
+
.collect();
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
/** Get a TOTP enrollment by its ID. */
|
|
503
|
+
export const totpGetById = query({
|
|
504
|
+
args: { totpId: v.id("totp") },
|
|
505
|
+
handler: async (ctx, { totpId }) => {
|
|
506
|
+
return await ctx.db.get(totpId);
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/** Mark a TOTP enrollment as verified (setup complete). */
|
|
511
|
+
export const totpMarkVerified = mutation({
|
|
512
|
+
args: { totpId: v.id("totp"), lastUsedAt: v.number() },
|
|
513
|
+
handler: async (ctx, { totpId, lastUsedAt }) => {
|
|
514
|
+
await ctx.db.patch(totpId, { verified: true, lastUsedAt });
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
/** Update a TOTP enrollment's last used timestamp. */
|
|
519
|
+
export const totpUpdateLastUsed = mutation({
|
|
520
|
+
args: { totpId: v.id("totp"), lastUsedAt: v.number() },
|
|
521
|
+
handler: async (ctx, { totpId, lastUsedAt }) => {
|
|
522
|
+
await ctx.db.patch(totpId, { lastUsedAt });
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
/** Delete a TOTP enrollment. */
|
|
527
|
+
export const totpDelete = mutation({
|
|
528
|
+
args: { totpId: v.id("totp") },
|
|
529
|
+
handler: async (ctx, { totpId }) => {
|
|
530
|
+
await ctx.db.delete(totpId);
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
|
|
363
534
|
// ============================================================================
|
|
364
535
|
// Rate Limits
|
|
365
536
|
// ============================================================================
|
|
@@ -629,8 +800,8 @@ export const memberUpdate = mutation({
|
|
|
629
800
|
export const inviteCreate = mutation({
|
|
630
801
|
args: {
|
|
631
802
|
groupId: v.optional(v.id("group")),
|
|
632
|
-
invitedByUserId: v.id("user"),
|
|
633
|
-
email: v.string(),
|
|
803
|
+
invitedByUserId: v.optional(v.id("user")),
|
|
804
|
+
email: v.optional(v.string()),
|
|
634
805
|
tokenHash: v.string(),
|
|
635
806
|
role: v.optional(v.string()),
|
|
636
807
|
status: v.union(
|
|
@@ -639,42 +810,48 @@ export const inviteCreate = mutation({
|
|
|
639
810
|
v.literal("revoked"),
|
|
640
811
|
v.literal("expired"),
|
|
641
812
|
),
|
|
642
|
-
expiresTime: v.number(),
|
|
813
|
+
expiresTime: v.optional(v.number()),
|
|
643
814
|
extend: v.optional(v.any()),
|
|
644
815
|
},
|
|
645
816
|
handler: async (ctx, args) => {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
817
|
+
// Only check for duplicates when an email is provided.
|
|
818
|
+
// CLI-generated invites (no email) are always allowed.
|
|
819
|
+
if (args.email !== undefined) {
|
|
820
|
+
if (args.groupId !== undefined) {
|
|
821
|
+
const existingGroupInvite = await ctx.db
|
|
822
|
+
.query("invite")
|
|
823
|
+
.withIndex("groupIdAndStatus", (q) =>
|
|
824
|
+
q.eq("groupId", args.groupId).eq("status", "pending"),
|
|
825
|
+
)
|
|
826
|
+
.filter((q) => q.eq(q.field("email"), args.email))
|
|
827
|
+
.first();
|
|
828
|
+
if (existingGroupInvite !== null) {
|
|
829
|
+
throw new ConvexError({
|
|
830
|
+
code: "DUPLICATE_INVITE",
|
|
831
|
+
message:
|
|
832
|
+
"A pending invite already exists for this email in this group",
|
|
833
|
+
email: args.email,
|
|
834
|
+
groupId: args.groupId,
|
|
835
|
+
existingInviteId: existingGroupInvite._id,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
const existingPlatformInvite = await ctx.db
|
|
840
|
+
.query("invite")
|
|
841
|
+
.withIndex("emailAndStatus", (q) =>
|
|
842
|
+
q.eq("email", args.email).eq("status", "pending"),
|
|
843
|
+
)
|
|
844
|
+
.filter((q) => q.eq(q.field("groupId"), undefined))
|
|
845
|
+
.first();
|
|
846
|
+
if (existingPlatformInvite !== null) {
|
|
847
|
+
throw new ConvexError({
|
|
848
|
+
code: "DUPLICATE_INVITE",
|
|
849
|
+
message:
|
|
850
|
+
"A pending platform invite already exists for this email",
|
|
851
|
+
email: args.email,
|
|
852
|
+
existingInviteId: existingPlatformInvite._id,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
678
855
|
}
|
|
679
856
|
}
|
|
680
857
|
return await ctx.db.insert("invite", args);
|
|
@@ -689,6 +866,17 @@ export const inviteGet = query({
|
|
|
689
866
|
},
|
|
690
867
|
});
|
|
691
868
|
|
|
869
|
+
/** Retrieve an invite by its token hash. Returns `null` if not found. */
|
|
870
|
+
export const inviteGetByTokenHash = query({
|
|
871
|
+
args: { tokenHash: v.string() },
|
|
872
|
+
handler: async (ctx, { tokenHash }) => {
|
|
873
|
+
return await ctx.db
|
|
874
|
+
.query("invite")
|
|
875
|
+
.withIndex("tokenHash", (q) => q.eq("tokenHash", tokenHash))
|
|
876
|
+
.unique();
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
|
|
692
880
|
/**
|
|
693
881
|
* List invites, optionally filtered by group and/or status.
|
|
694
882
|
* Both `groupId` and `status` are optional filters.
|
|
@@ -740,8 +928,11 @@ export const inviteList = query({
|
|
|
740
928
|
* The caller is responsible for creating the corresponding member record.
|
|
741
929
|
*/
|
|
742
930
|
export const inviteAccept = mutation({
|
|
743
|
-
args: {
|
|
744
|
-
|
|
931
|
+
args: {
|
|
932
|
+
inviteId: v.id("invite"),
|
|
933
|
+
acceptedByUserId: v.optional(v.id("user")),
|
|
934
|
+
},
|
|
935
|
+
handler: async (ctx, { inviteId, acceptedByUserId }) => {
|
|
745
936
|
const invite = await ctx.db.get(inviteId);
|
|
746
937
|
if (invite === null) {
|
|
747
938
|
throw new ConvexError({
|
|
@@ -761,6 +952,7 @@ export const inviteAccept = mutation({
|
|
|
761
952
|
await ctx.db.patch(inviteId, {
|
|
762
953
|
status: "accepted",
|
|
763
954
|
acceptedTime: Date.now(),
|
|
955
|
+
...(acceptedByUserId ? { acceptedByUserId } : {}),
|
|
764
956
|
});
|
|
765
957
|
},
|
|
766
958
|
});
|
|
@@ -793,3 +985,5 @@ export const inviteRevoke = mutation({
|
|
|
793
985
|
await ctx.db.patch(inviteId, { status: "revoked" });
|
|
794
986
|
},
|
|
795
987
|
});
|
|
988
|
+
|
|
989
|
+
|
package/src/component/schema.ts
CHANGED
|
@@ -96,6 +96,61 @@ export default defineSchema({
|
|
|
96
96
|
signature: v.optional(v.string()),
|
|
97
97
|
}).index("signature", ["signature"]),
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* WebAuthn passkey credentials. Each credential links a user to a
|
|
101
|
+
* registered authenticator (Touch ID, Face ID, security key, etc.).
|
|
102
|
+
* A user can have multiple passkeys across different devices.
|
|
103
|
+
*/
|
|
104
|
+
passkey: defineTable({
|
|
105
|
+
userId: v.id("user"),
|
|
106
|
+
/** Base64url-encoded credential ID from the authenticator. */
|
|
107
|
+
credentialId: v.string(),
|
|
108
|
+
/** Public key bytes (SEC1 uncompressed for EC, SPKI for RSA). */
|
|
109
|
+
publicKey: v.bytes(),
|
|
110
|
+
/** COSE algorithm identifier (-7 for ES256, -257 for RS256, -8 for EdDSA). */
|
|
111
|
+
algorithm: v.number(),
|
|
112
|
+
/** Signature counter for clone detection. Many authenticators return 0. */
|
|
113
|
+
counter: v.number(),
|
|
114
|
+
/** Authenticator transport hints (e.g. "internal", "hybrid", "usb", "ble", "nfc"). */
|
|
115
|
+
transports: v.optional(v.array(v.string())),
|
|
116
|
+
/** Whether this is a single-device or multi-device (synced) credential. */
|
|
117
|
+
deviceType: v.string(),
|
|
118
|
+
/** Whether the credential is backed up (synced passkey). */
|
|
119
|
+
backedUp: v.boolean(),
|
|
120
|
+
/** User-assigned friendly name (e.g. "MacBook Touch ID"). */
|
|
121
|
+
name: v.optional(v.string()),
|
|
122
|
+
createdAt: v.number(),
|
|
123
|
+
lastUsedAt: v.optional(v.number()),
|
|
124
|
+
})
|
|
125
|
+
.index("userId", ["userId"])
|
|
126
|
+
.index("credentialId", ["credentialId"]),
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* TOTP two-factor authentication secrets. Each record links a user to
|
|
130
|
+
* an authenticator app. A user can have multiple TOTP enrollments
|
|
131
|
+
* (e.g. different authenticator apps) but typically has one.
|
|
132
|
+
*
|
|
133
|
+
* The `verified` flag indicates whether the user has completed setup
|
|
134
|
+
* by successfully entering a code from their authenticator app.
|
|
135
|
+
* Unverified enrollments are in-progress setup that can be discarded.
|
|
136
|
+
*/
|
|
137
|
+
totp: defineTable({
|
|
138
|
+
userId: v.id("user"),
|
|
139
|
+
/** Raw TOTP secret key bytes. */
|
|
140
|
+
secret: v.bytes(),
|
|
141
|
+
/** Number of digits in each code (typically 6). */
|
|
142
|
+
digits: v.number(),
|
|
143
|
+
/** Time period in seconds for code rotation (typically 30). */
|
|
144
|
+
period: v.number(),
|
|
145
|
+
/** Whether setup has been confirmed with a valid code. */
|
|
146
|
+
verified: v.boolean(),
|
|
147
|
+
/** User-assigned friendly name (e.g. "Google Authenticator"). */
|
|
148
|
+
name: v.optional(v.string()),
|
|
149
|
+
createdAt: v.number(),
|
|
150
|
+
lastUsedAt: v.optional(v.number()),
|
|
151
|
+
})
|
|
152
|
+
.index("userId", ["userId"]),
|
|
153
|
+
|
|
99
154
|
/**
|
|
100
155
|
* Rate limit tracking for OTP and password sign-in attempts.
|
|
101
156
|
*/
|
|
@@ -136,14 +191,17 @@ export default defineSchema({
|
|
|
136
191
|
.index("userId", ["userId"]),
|
|
137
192
|
|
|
138
193
|
/**
|
|
139
|
-
*
|
|
140
|
-
* invitations to
|
|
141
|
-
*
|
|
194
|
+
* Invitations. Tracks pending, accepted, revoked, and expired
|
|
195
|
+
* invitations. Optionally scoped to a group via `groupId`, or
|
|
196
|
+
* platform-level when `groupId` is omitted.
|
|
197
|
+
*
|
|
198
|
+
* `email` and `invitedByUserId` are optional to support CLI-generated
|
|
199
|
+
* invite links where neither is known upfront (e.g. portal admin invites).
|
|
142
200
|
*/
|
|
143
201
|
invite: defineTable({
|
|
144
202
|
groupId: v.optional(v.id("group")),
|
|
145
|
-
invitedByUserId: v.id("user"),
|
|
146
|
-
email: v.string(),
|
|
203
|
+
invitedByUserId: v.optional(v.id("user")),
|
|
204
|
+
email: v.optional(v.string()),
|
|
147
205
|
tokenHash: v.string(),
|
|
148
206
|
role: v.optional(v.string()),
|
|
149
207
|
status: v.union(
|
|
@@ -152,7 +210,7 @@ export default defineSchema({
|
|
|
152
210
|
v.literal("revoked"),
|
|
153
211
|
v.literal("expired"),
|
|
154
212
|
),
|
|
155
|
-
expiresTime: v.number(),
|
|
213
|
+
expiresTime: v.optional(v.number()),
|
|
156
214
|
acceptedByUserId: v.optional(v.id("user")),
|
|
157
215
|
acceptedTime: v.optional(v.number()),
|
|
158
216
|
extend: v.optional(v.any()),
|
|
@@ -162,5 +220,10 @@ export default defineSchema({
|
|
|
162
220
|
.index("emailAndStatus", ["email", "status"])
|
|
163
221
|
.index("invitedByUserIdAndStatus", ["invitedByUserId", "status"])
|
|
164
222
|
.index("groupId", ["groupId"])
|
|
165
|
-
.index("groupIdAndStatus", ["groupId", "status"])
|
|
223
|
+
.index("groupIdAndStatus", ["groupId", "status"])
|
|
224
|
+
.index("roleAndStatusAndAcceptedByUserId", [
|
|
225
|
+
"role",
|
|
226
|
+
"status",
|
|
227
|
+
"acceptedByUserId",
|
|
228
|
+
]),
|
|
166
229
|
});
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configure {@link
|
|
2
|
+
* Configure {@link anonymous} provider given an {@link AnonymousConfig}.
|
|
3
3
|
*
|
|
4
4
|
* ```ts
|
|
5
|
-
* import
|
|
6
|
-
* import {
|
|
5
|
+
* import anonymous from "@robelest/convex-auth/providers/anonymous";
|
|
6
|
+
* import { Auth } from "@robelest/convex-auth/component";
|
|
7
7
|
*
|
|
8
|
-
* export const { auth, signIn, signOut, store } =
|
|
9
|
-
* providers: [
|
|
8
|
+
* export const { auth, signIn, signOut, store } = Auth({
|
|
9
|
+
* providers: [anonymous],
|
|
10
10
|
* });
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import
|
|
16
|
+
import credentials from "@robelest/convex-auth/providers/credentials";
|
|
17
17
|
import {
|
|
18
18
|
GenericActionCtxWithAuthConfig,
|
|
19
|
-
createAccount,
|
|
20
19
|
} from "@robelest/convex-auth/component";
|
|
21
20
|
import {
|
|
22
21
|
DocumentByName,
|
|
@@ -26,12 +25,12 @@ import {
|
|
|
26
25
|
import { Value } from "convex/values";
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
* The available options to an {@link
|
|
28
|
+
* The available options to an {@link anonymous} provider for Convex Auth.
|
|
30
29
|
*/
|
|
31
30
|
export interface AnonymousConfig<DataModel extends GenericDataModel> {
|
|
32
31
|
/**
|
|
33
32
|
* Uniquely identifies the provider, allowing to use
|
|
34
|
-
* multiple different {@link
|
|
33
|
+
* multiple different {@link anonymous} providers.
|
|
35
34
|
*/
|
|
36
35
|
id?: string;
|
|
37
36
|
/**
|
|
@@ -62,11 +61,11 @@ export default function anonymous<DataModel extends GenericDataModel>(
|
|
|
62
61
|
config: AnonymousConfig<DataModel> = {},
|
|
63
62
|
) {
|
|
64
63
|
const provider = config.id ?? "anonymous";
|
|
65
|
-
return
|
|
64
|
+
return credentials<DataModel>({
|
|
66
65
|
id: "anonymous",
|
|
67
66
|
authorize: async (params, ctx) => {
|
|
68
67
|
const profile = config.profile?.(params, ctx) ?? { isAnonymous: true };
|
|
69
|
-
const { user } = await
|
|
68
|
+
const { user } = await ctx.auth.account.create(ctx, {
|
|
70
69
|
provider,
|
|
71
70
|
account: { id: crypto.randomUUID() },
|
|
72
71
|
profile: profile as any,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configure {@link
|
|
2
|
+
* Configure {@link credentials} provider given a {@link CredentialsUserConfig}.
|
|
3
3
|
*
|
|
4
4
|
* This is for a very custom authentication implementation, often you can
|
|
5
|
-
* use the [`
|
|
5
|
+
* use the [`password`](https://labs.convex.dev/auth/api_reference/providers/password) provider instead.
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
|
-
* import
|
|
9
|
-
* import {
|
|
8
|
+
* import credentials from "@robelest/convex-auth/providers/credentials";
|
|
9
|
+
* import { Auth } from "@robelest/convex-auth/component";
|
|
10
10
|
*
|
|
11
|
-
* export const { auth, signIn, signOut, store } =
|
|
11
|
+
* export const { auth, signIn, signOut, store } = Auth({
|
|
12
12
|
* providers: [
|
|
13
|
-
*
|
|
13
|
+
* credentials({
|
|
14
14
|
* authorize: async (credentials, ctx) => {
|
|
15
15
|
* // Your custom logic here...
|
|
16
16
|
* },
|
|
@@ -31,14 +31,14 @@ import { GenericDataModel } from "convex/server";
|
|
|
31
31
|
import { GenericId, Value } from "convex/values";
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* The available options to a {@link
|
|
34
|
+
* The available options to a {@link credentials} provider for Convex Auth.
|
|
35
35
|
*/
|
|
36
|
-
export interface
|
|
36
|
+
export interface CredentialsUserConfig<
|
|
37
37
|
DataModel extends GenericDataModel = GenericDataModel,
|
|
38
38
|
> {
|
|
39
39
|
/**
|
|
40
40
|
* Uniquely identifies the provider, allowing to use
|
|
41
|
-
* multiple different {@link
|
|
41
|
+
* multiple different {@link credentials} providers.
|
|
42
42
|
*/
|
|
43
43
|
id?: string;
|
|
44
44
|
/**
|
|
@@ -95,8 +95,8 @@ export interface ConvexCredentialsUserConfig<
|
|
|
95
95
|
* The Credentials provider allows you to handle signing in with arbitrary credentials,
|
|
96
96
|
* such as a username and password, domain, or two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
|
|
97
97
|
*/
|
|
98
|
-
export default function
|
|
99
|
-
config:
|
|
98
|
+
export default function credentials<DataModel extends GenericDataModel>(
|
|
99
|
+
config: CredentialsUserConfig<DataModel>,
|
|
100
100
|
): ConvexCredentialsConfig {
|
|
101
101
|
return {
|
|
102
102
|
id: "credentials",
|
|
@@ -19,19 +19,19 @@ import { EmailConfig, EmailUserConfig } from "../server/types.js";
|
|
|
19
19
|
* you can override the `authorize` method to skip the check:
|
|
20
20
|
*
|
|
21
21
|
* ```ts
|
|
22
|
-
* import
|
|
23
|
-
* import {
|
|
22
|
+
* import email from "@robelest/convex-auth/providers/email";
|
|
23
|
+
* import { Auth } from "@robelest/convex-auth/component";
|
|
24
24
|
*
|
|
25
|
-
* export const { auth, signIn, signOut, store } =
|
|
25
|
+
* export const { auth, signIn, signOut, store } = Auth({
|
|
26
26
|
* providers: [
|
|
27
|
-
*
|
|
27
|
+
* email({ authorize: undefined }),
|
|
28
28
|
* ],
|
|
29
29
|
* });
|
|
30
30
|
* ```
|
|
31
31
|
*
|
|
32
32
|
* Make sure the token has high enough entropy to be secure.
|
|
33
33
|
*/
|
|
34
|
-
export function
|
|
34
|
+
export default function email<DataModel extends GenericDataModel>(
|
|
35
35
|
config: EmailUserConfig<DataModel> &
|
|
36
36
|
Pick<EmailConfig, "sendVerificationRequest">,
|
|
37
37
|
): EmailConfig<DataModel> {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PasskeyProviderConfig } from "../server/types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Passkey (WebAuthn) authentication provider.
|
|
5
|
+
*
|
|
6
|
+
* Enables passwordless authentication via biometrics, security keys,
|
|
7
|
+
* and synced passkeys using the Web Authentication API.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import passkey from "@robelest/convex-auth/providers/passkey";
|
|
11
|
+
*
|
|
12
|
+
* export const { auth, signIn, signOut, store } = Auth({
|
|
13
|
+
* component: components.auth,
|
|
14
|
+
* providers: [passkey()],
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @param config Optional configuration for the relying party and credential options.
|
|
19
|
+
*/
|
|
20
|
+
export default function passkey(
|
|
21
|
+
config?: Partial<PasskeyProviderConfig["options"]>,
|
|
22
|
+
): PasskeyProviderConfig {
|
|
23
|
+
return {
|
|
24
|
+
id: "passkey",
|
|
25
|
+
type: "passkey",
|
|
26
|
+
options: {
|
|
27
|
+
attestation: "none",
|
|
28
|
+
userVerification: "required",
|
|
29
|
+
residentKey: "preferred",
|
|
30
|
+
algorithms: [-7, -257], // ES256, RS256
|
|
31
|
+
challengeExpirationMs: 300_000, // 5 minutes
|
|
32
|
+
...config,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|