@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.
Files changed (145) hide show
  1. package/dist/bin.cjs +466 -63
  2. package/dist/client/index.d.ts +211 -30
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +673 -59
  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 +93 -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 -3
  15. package/dist/component/index.d.ts.map +1 -1
  16. package/dist/component/index.js +5 -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 +193 -9
  23. package/dist/component/public.d.ts.map +1 -1
  24. package/dist/component/public.js +204 -33
  25. package/dist/component/public.js.map +1 -1
  26. package/dist/component/schema.d.ts +89 -9
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +68 -7
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/providers/{Anonymous.d.ts → anonymous.d.ts} +8 -8
  31. package/dist/providers/{Anonymous.d.ts.map → anonymous.d.ts.map} +1 -1
  32. package/dist/providers/{Anonymous.js → anonymous.js} +9 -10
  33. package/dist/providers/anonymous.js.map +1 -0
  34. package/dist/providers/{ConvexCredentials.d.ts → credentials.d.ts} +11 -11
  35. package/dist/providers/credentials.d.ts.map +1 -0
  36. package/dist/providers/{ConvexCredentials.js → credentials.js} +8 -8
  37. package/dist/providers/credentials.js.map +1 -0
  38. package/dist/providers/{Email.d.ts → email.d.ts} +6 -6
  39. package/dist/providers/email.d.ts.map +1 -0
  40. package/dist/providers/{Email.js → email.js} +6 -6
  41. package/dist/providers/email.js.map +1 -0
  42. package/dist/providers/passkey.d.ts +20 -0
  43. package/dist/providers/passkey.d.ts.map +1 -0
  44. package/dist/providers/passkey.js +32 -0
  45. package/dist/providers/passkey.js.map +1 -0
  46. package/dist/providers/{Password.d.ts → password.d.ts} +10 -10
  47. package/dist/providers/{Password.d.ts.map → password.d.ts.map} +1 -1
  48. package/dist/providers/{Password.js → password.js} +19 -20
  49. package/dist/providers/password.js.map +1 -0
  50. package/dist/providers/{Phone.d.ts → phone.d.ts} +3 -3
  51. package/dist/providers/{Phone.d.ts.map → phone.d.ts.map} +1 -1
  52. package/dist/providers/{Phone.js → phone.js} +3 -3
  53. package/dist/providers/{Phone.js.map → phone.js.map} +1 -1
  54. package/dist/providers/totp.d.ts +14 -0
  55. package/dist/providers/totp.d.ts.map +1 -0
  56. package/dist/providers/totp.js +23 -0
  57. package/dist/providers/totp.js.map +1 -0
  58. package/dist/server/convex-auth.d.ts +243 -0
  59. package/dist/server/convex-auth.d.ts.map +1 -0
  60. package/dist/server/convex-auth.js +365 -0
  61. package/dist/server/convex-auth.js.map +1 -0
  62. package/dist/server/implementation/index.d.ts +153 -166
  63. package/dist/server/implementation/index.d.ts.map +1 -1
  64. package/dist/server/implementation/index.js +162 -105
  65. package/dist/server/implementation/index.js.map +1 -1
  66. package/dist/server/implementation/passkey.d.ts +33 -0
  67. package/dist/server/implementation/passkey.d.ts.map +1 -0
  68. package/dist/server/implementation/passkey.js +450 -0
  69. package/dist/server/implementation/passkey.js.map +1 -0
  70. package/dist/server/implementation/redirects.d.ts.map +1 -1
  71. package/dist/server/implementation/redirects.js +4 -9
  72. package/dist/server/implementation/redirects.js.map +1 -1
  73. package/dist/server/implementation/sessions.d.ts +2 -20
  74. package/dist/server/implementation/sessions.d.ts.map +1 -1
  75. package/dist/server/implementation/sessions.js +2 -20
  76. package/dist/server/implementation/sessions.js.map +1 -1
  77. package/dist/server/implementation/signIn.d.ts +13 -0
  78. package/dist/server/implementation/signIn.d.ts.map +1 -1
  79. package/dist/server/implementation/signIn.js +26 -1
  80. package/dist/server/implementation/signIn.js.map +1 -1
  81. package/dist/server/implementation/totp.d.ts +40 -0
  82. package/dist/server/implementation/totp.d.ts.map +1 -0
  83. package/dist/server/implementation/totp.js +211 -0
  84. package/dist/server/implementation/totp.js.map +1 -0
  85. package/dist/server/index.d.ts +18 -0
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js +255 -0
  88. package/dist/server/index.js.map +1 -1
  89. package/dist/server/portal-email.d.ts +19 -0
  90. package/dist/server/portal-email.d.ts.map +1 -0
  91. package/dist/server/portal-email.js +89 -0
  92. package/dist/server/portal-email.js.map +1 -0
  93. package/dist/server/portal.d.ts +116 -0
  94. package/dist/server/portal.d.ts.map +1 -0
  95. package/dist/server/portal.js +294 -0
  96. package/dist/server/portal.js.map +1 -0
  97. package/dist/server/provider_utils.d.ts +1 -1
  98. package/dist/server/provider_utils.d.ts.map +1 -1
  99. package/dist/server/provider_utils.js +39 -1
  100. package/dist/server/provider_utils.js.map +1 -1
  101. package/dist/server/types.d.ts +128 -11
  102. package/dist/server/types.d.ts.map +1 -1
  103. package/package.json +7 -7
  104. package/src/cli/index.ts +48 -6
  105. package/src/cli/portal-link.ts +112 -0
  106. package/src/cli/portal-upload.ts +411 -0
  107. package/src/client/index.ts +823 -109
  108. package/src/component/_generated/api.ts +72 -1
  109. package/src/component/_generated/component.ts +180 -4
  110. package/src/component/convex.config.ts +3 -0
  111. package/src/component/index.ts +5 -10
  112. package/src/component/portalBridge.ts +116 -0
  113. package/src/component/public.ts +231 -37
  114. package/src/component/schema.ts +70 -7
  115. package/src/providers/{Anonymous.ts → anonymous.ts} +10 -11
  116. package/src/providers/{ConvexCredentials.ts → credentials.ts} +11 -11
  117. package/src/providers/{Email.ts → email.ts} +5 -5
  118. package/src/providers/passkey.ts +35 -0
  119. package/src/providers/{Password.ts → password.ts} +22 -27
  120. package/src/providers/{Phone.ts → phone.ts} +2 -2
  121. package/src/providers/totp.ts +26 -0
  122. package/src/server/convex-auth.ts +470 -0
  123. package/src/server/implementation/index.ts +228 -239
  124. package/src/server/implementation/passkey.ts +650 -0
  125. package/src/server/implementation/redirects.ts +4 -11
  126. package/src/server/implementation/sessions.ts +2 -20
  127. package/src/server/implementation/signIn.ts +39 -1
  128. package/src/server/implementation/totp.ts +366 -0
  129. package/src/server/index.ts +373 -0
  130. package/src/server/portal-email.ts +95 -0
  131. package/src/server/portal.ts +375 -0
  132. package/src/server/provider_utils.ts +42 -1
  133. package/src/server/types.ts +161 -10
  134. package/dist/providers/Anonymous.js.map +0 -1
  135. package/dist/providers/ConvexCredentials.d.ts.map +0 -1
  136. package/dist/providers/ConvexCredentials.js.map +0 -1
  137. package/dist/providers/Email.d.ts.map +0 -1
  138. package/dist/providers/Email.js.map +0 -1
  139. package/dist/providers/Password.js.map +0 -1
  140. package/providers/Anonymous/package.json +0 -6
  141. package/providers/ConvexCredentials/package.json +0 -6
  142. package/providers/Email/package.json +0 -6
  143. package/providers/Password/package.json +0 -6
  144. package/providers/Phone/package.json +0 -6
  145. package/server/package.json +0 -6
@@ -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
- if (args.groupId !== undefined) {
647
- const existingGroupInvite = await ctx.db
648
- .query("invite")
649
- .withIndex("groupIdAndStatus", (q) =>
650
- q.eq("groupId", args.groupId).eq("status", "pending"),
651
- )
652
- .filter((q) => q.eq(q.field("email"), args.email))
653
- .first();
654
- if (existingGroupInvite !== null) {
655
- throw new ConvexError({
656
- code: "DUPLICATE_INVITE",
657
- message: "A pending invite already exists for this email in this group",
658
- email: args.email,
659
- groupId: args.groupId,
660
- existingInviteId: existingGroupInvite._id,
661
- });
662
- }
663
- } else {
664
- const existingPlatformInvite = await ctx.db
665
- .query("invite")
666
- .withIndex("emailAndStatus", (q) =>
667
- q.eq("email", args.email).eq("status", "pending"),
668
- )
669
- .filter((q) => q.eq(q.field("groupId"), undefined))
670
- .first();
671
- if (existingPlatformInvite !== null) {
672
- throw new ConvexError({
673
- code: "DUPLICATE_INVITE",
674
- message: "A pending platform invite already exists for this email",
675
- email: args.email,
676
- existingInviteId: existingPlatformInvite._id,
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: { inviteId: v.id("invite") },
744
- handler: async (ctx, { inviteId }) => {
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
+
@@ -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
- * Group invitations. Tracks pending, accepted, revoked, and expired
140
- * invitations to join a group. Uses a hashed token for secure
141
- * invitation links.
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 Anonymous} provider given an {@link AnonymousConfig}.
2
+ * Configure {@link anonymous} provider given an {@link AnonymousConfig}.
3
3
  *
4
4
  * ```ts
5
- * import { Anonymous } from "@robelest/convex-auth/providers/Anonymous";
6
- * import { convexAuth } from "@robelest/convex-auth/component";
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 } = convexAuth({
9
- * providers: [Anonymous],
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 convexCredentials from "@robelest/convex-auth/providers/ConvexCredentials";
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 Anonymous} provider for Convex Auth.
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 Anonymous} providers.
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 convexCredentials<DataModel>({
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 createAccount(ctx, {
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 ConvexCredentials} provider given a {@link ConvexCredentialsUserConfig}.
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 [`Password`](https://labs.convex.dev/auth/api_reference/providers/Password) provider instead.
5
+ * use the [`password`](https://labs.convex.dev/auth/api_reference/providers/password) provider instead.
6
6
  *
7
7
  * ```ts
8
- * import ConvexCredentials from "@robelest/convex-auth/providers/ConvexCredentials";
9
- * import { convexAuth } from "@robelest/convex-auth/component";
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 } = convexAuth({
11
+ * export const { auth, signIn, signOut, store } = Auth({
12
12
  * providers: [
13
- * ConvexCredentials({
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 ConvexCredentials} provider for Convex Auth.
34
+ * The available options to a {@link credentials} provider for Convex Auth.
35
35
  */
36
- export interface ConvexCredentialsUserConfig<
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 ConvexCredentials} providers.
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 convexCredentials<DataModel extends GenericDataModel>(
99
- config: ConvexCredentialsUserConfig<DataModel>,
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 Email from "@robelest/convex-auth/providers/Email";
23
- * import { convexAuth } from "@robelest/convex-auth/component";
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 } = convexAuth({
25
+ * export const { auth, signIn, signOut, store } = Auth({
26
26
  * providers: [
27
- * Email({ authorize: undefined }),
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 Email<DataModel extends GenericDataModel>(
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
+ }