convex-zen 0.0.1

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 (142) hide show
  1. package/dist/cli/generate.d.ts +14 -0
  2. package/dist/cli/generate.d.ts.map +1 -0
  3. package/dist/cli/generate.js +297 -0
  4. package/dist/cli/generate.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +111 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/client/index.d.ts +300 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +434 -0
  12. package/dist/client/index.js.map +1 -0
  13. package/dist/client/plugins/admin.d.ts +92 -0
  14. package/dist/client/plugins/admin.d.ts.map +1 -0
  15. package/dist/client/plugins/admin.js +165 -0
  16. package/dist/client/plugins/admin.js.map +1 -0
  17. package/dist/client/primitives.d.ts +57 -0
  18. package/dist/client/primitives.d.ts.map +1 -0
  19. package/dist/client/primitives.js +64 -0
  20. package/dist/client/primitives.js.map +1 -0
  21. package/dist/client/providers.d.ts +14 -0
  22. package/dist/client/providers.d.ts.map +1 -0
  23. package/dist/client/providers.js +25 -0
  24. package/dist/client/providers.js.map +1 -0
  25. package/dist/client/react.d.ts +23 -0
  26. package/dist/client/react.d.ts.map +1 -0
  27. package/dist/client/react.js +48 -0
  28. package/dist/client/react.js.map +1 -0
  29. package/dist/client/tanstack-start-client-plugins.d.ts +34 -0
  30. package/dist/client/tanstack-start-client-plugins.d.ts.map +1 -0
  31. package/dist/client/tanstack-start-client-plugins.js +32 -0
  32. package/dist/client/tanstack-start-client-plugins.js.map +1 -0
  33. package/dist/client/tanstack-start-client.d.ts +52 -0
  34. package/dist/client/tanstack-start-client.d.ts.map +1 -0
  35. package/dist/client/tanstack-start-client.js +130 -0
  36. package/dist/client/tanstack-start-client.js.map +1 -0
  37. package/dist/client/tanstack-start-plugins.d.ts +27 -0
  38. package/dist/client/tanstack-start-plugins.d.ts.map +1 -0
  39. package/dist/client/tanstack-start-plugins.js +145 -0
  40. package/dist/client/tanstack-start-plugins.js.map +1 -0
  41. package/dist/client/tanstack-start.d.ts +130 -0
  42. package/dist/client/tanstack-start.d.ts.map +1 -0
  43. package/dist/client/tanstack-start.js +331 -0
  44. package/dist/client/tanstack-start.js.map +1 -0
  45. package/dist/component/_generated/api.d.ts +50 -0
  46. package/dist/component/_generated/api.d.ts.map +1 -0
  47. package/dist/component/_generated/api.js +31 -0
  48. package/dist/component/_generated/api.js.map +1 -0
  49. package/dist/component/_generated/component.d.ts +92 -0
  50. package/dist/component/_generated/component.d.ts.map +1 -0
  51. package/dist/component/_generated/component.js +11 -0
  52. package/dist/component/_generated/component.js.map +1 -0
  53. package/dist/component/_generated/dataModel.d.ts +46 -0
  54. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  55. package/dist/component/_generated/dataModel.js +11 -0
  56. package/dist/component/_generated/dataModel.js.map +1 -0
  57. package/dist/component/_generated/server.d.ts +121 -0
  58. package/dist/component/_generated/server.d.ts.map +1 -0
  59. package/dist/component/_generated/server.js +78 -0
  60. package/dist/component/_generated/server.js.map +1 -0
  61. package/dist/component/convex.config.d.ts +3 -0
  62. package/dist/component/convex.config.d.ts.map +1 -0
  63. package/dist/component/convex.config.js +4 -0
  64. package/dist/component/convex.config.js.map +1 -0
  65. package/dist/component/core/sessions.d.ts +33 -0
  66. package/dist/component/core/sessions.d.ts.map +1 -0
  67. package/dist/component/core/sessions.js +186 -0
  68. package/dist/component/core/sessions.js.map +1 -0
  69. package/dist/component/core/users.d.ts +19 -0
  70. package/dist/component/core/users.d.ts.map +1 -0
  71. package/dist/component/core/users.js +154 -0
  72. package/dist/component/core/users.js.map +1 -0
  73. package/dist/component/core/verifications.d.ts +34 -0
  74. package/dist/component/core/verifications.d.ts.map +1 -0
  75. package/dist/component/core/verifications.js +135 -0
  76. package/dist/component/core/verifications.js.map +1 -0
  77. package/dist/component/gateway.d.ts +16 -0
  78. package/dist/component/gateway.d.ts.map +1 -0
  79. package/dist/component/gateway.js +229 -0
  80. package/dist/component/gateway.js.map +1 -0
  81. package/dist/component/lib/crypto.d.ts +24 -0
  82. package/dist/component/lib/crypto.d.ts.map +1 -0
  83. package/dist/component/lib/crypto.js +57 -0
  84. package/dist/component/lib/crypto.js.map +1 -0
  85. package/dist/component/lib/rateLimit.d.ts +26 -0
  86. package/dist/component/lib/rateLimit.d.ts.map +1 -0
  87. package/dist/component/lib/rateLimit.js +96 -0
  88. package/dist/component/lib/rateLimit.js.map +1 -0
  89. package/dist/component/lib/validators.d.ts +19 -0
  90. package/dist/component/lib/validators.d.ts.map +1 -0
  91. package/dist/component/lib/validators.js +12 -0
  92. package/dist/component/lib/validators.js.map +1 -0
  93. package/dist/component/plugins/admin.d.ts +72 -0
  94. package/dist/component/plugins/admin.d.ts.map +1 -0
  95. package/dist/component/plugins/admin.js +152 -0
  96. package/dist/component/plugins/admin.js.map +1 -0
  97. package/dist/component/providers/emailPassword.d.ts +49 -0
  98. package/dist/component/providers/emailPassword.d.ts.map +1 -0
  99. package/dist/component/providers/emailPassword.js +316 -0
  100. package/dist/component/providers/emailPassword.js.map +1 -0
  101. package/dist/component/providers/oauth.d.ts +33 -0
  102. package/dist/component/providers/oauth.d.ts.map +1 -0
  103. package/dist/component/providers/oauth.js +256 -0
  104. package/dist/component/providers/oauth.js.map +1 -0
  105. package/dist/component/schema.d.ts +132 -0
  106. package/dist/component/schema.d.ts.map +1 -0
  107. package/dist/component/schema.js +82 -0
  108. package/dist/component/schema.js.map +1 -0
  109. package/dist/types.d.ts +67 -0
  110. package/dist/types.d.ts.map +1 -0
  111. package/dist/types.js +5 -0
  112. package/dist/types.js.map +1 -0
  113. package/package.json +121 -0
  114. package/src/cli/generate.ts +360 -0
  115. package/src/cli/index.ts +133 -0
  116. package/src/client/index.ts +707 -0
  117. package/src/client/plugins/admin.ts +205 -0
  118. package/src/client/primitives.ts +100 -0
  119. package/src/client/providers.ts +35 -0
  120. package/src/client/react.ts +97 -0
  121. package/src/client/tanstack-start-client-plugins.ts +113 -0
  122. package/src/client/tanstack-start-client.ts +259 -0
  123. package/src/client/tanstack-start-plugins.ts +203 -0
  124. package/src/client/tanstack-start.ts +535 -0
  125. package/src/component/_generated/api.ts +70 -0
  126. package/src/component/_generated/component.ts +184 -0
  127. package/src/component/_generated/dataModel.ts +60 -0
  128. package/src/component/_generated/server.ts +156 -0
  129. package/src/component/convex.config.ts +5 -0
  130. package/src/component/core/sessions.ts +228 -0
  131. package/src/component/core/users.ts +199 -0
  132. package/src/component/core/verifications.ts +173 -0
  133. package/src/component/gateway.ts +321 -0
  134. package/src/component/lib/crypto.ts +63 -0
  135. package/src/component/lib/internalApi.ts +66 -0
  136. package/src/component/lib/rateLimit.ts +111 -0
  137. package/src/component/lib/validators.ts +12 -0
  138. package/src/component/plugins/admin.ts +178 -0
  139. package/src/component/providers/emailPassword.ts +374 -0
  140. package/src/component/providers/oauth.ts +324 -0
  141. package/src/component/schema.ts +88 -0
  142. package/src/types.ts +68 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Cryptographic utilities using Web Crypto API.
3
+ * All token generation uses crypto.getRandomValues for 256+ bit entropy.
4
+ * Tokens are stored as SHA-256 hashes only (never raw).
5
+ */
6
+
7
+ /** Generate a cryptographically secure 32-byte random hex token (256 bits). */
8
+ export function generateToken(): string {
9
+ const bytes = new Uint8Array(32);
10
+ crypto.getRandomValues(bytes);
11
+ return Array.from(bytes)
12
+ .map((b) => b.toString(16).padStart(2, "0"))
13
+ .join("");
14
+ }
15
+
16
+ /** SHA-256 hash a string token, returns hex string. */
17
+ export async function hashToken(token: string): Promise<string> {
18
+ const encoder = new TextEncoder();
19
+ const data = encoder.encode(token);
20
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
21
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
22
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
23
+ }
24
+
25
+ /** Base64url encode bytes (no padding). Used for PKCE code challenge. */
26
+ export function base64url(bytes: Uint8Array): string {
27
+ const base64 = btoa(String.fromCharCode(...bytes));
28
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
29
+ }
30
+
31
+ /** Generate a PKCE code verifier (32 random bytes, base64url encoded). */
32
+ export function generateCodeVerifier(): string {
33
+ const bytes = new Uint8Array(32);
34
+ crypto.getRandomValues(bytes);
35
+ return base64url(bytes);
36
+ }
37
+
38
+ /** Generate a PKCE code challenge (SHA-256 of verifier, base64url encoded). */
39
+ export async function generateCodeChallenge(verifier: string): Promise<string> {
40
+ const encoder = new TextEncoder();
41
+ const data = encoder.encode(verifier);
42
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
43
+ return base64url(new Uint8Array(hashBuffer));
44
+ }
45
+
46
+ /**
47
+ * Generate a cryptographically secure 8-character alphanumeric code.
48
+ * Uses rejection sampling to avoid modulo bias.
49
+ * Charset: A-Z, 0-9 (36 chars) — avoids ambiguous chars like 0/O, 1/I/l.
50
+ */
51
+ export function generateCode(): string {
52
+ const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 32 chars, power of 2
53
+ const bytes = new Uint8Array(8);
54
+ crypto.getRandomValues(bytes);
55
+ return Array.from(bytes)
56
+ .map((b) => charset[b & 31]) // 32 = 2^5, no bias since 256 / 32 = 8 exactly
57
+ .join("");
58
+ }
59
+
60
+ /** Generate a 32-byte random state token for OAuth (returned as hex). */
61
+ export function generateState(): string {
62
+ return generateToken();
63
+ }
@@ -0,0 +1,66 @@
1
+ import { anyApi } from "convex/server";
2
+ import type { FunctionReference } from "convex/server";
3
+
4
+ type InternalQuery = FunctionReference<"query", "internal">;
5
+ type InternalMutation = FunctionReference<"mutation", "internal">;
6
+ type InternalAction = FunctionReference<"action", "internal">;
7
+
8
+ export const internal = anyApi as unknown as {
9
+ core: {
10
+ sessions: {
11
+ create: InternalMutation;
12
+ getByToken: InternalQuery;
13
+ validate: InternalAction;
14
+ extend: InternalMutation;
15
+ invalidateByHash: InternalMutation;
16
+ invalidateByToken: InternalMutation;
17
+ invalidateAll: InternalMutation;
18
+ };
19
+ users: {
20
+ create: InternalMutation;
21
+ getByEmail: InternalQuery;
22
+ getById: InternalQuery;
23
+ update: InternalMutation;
24
+ createAccount: InternalMutation;
25
+ getAccount: InternalQuery;
26
+ updateAccount: InternalMutation;
27
+ };
28
+ verifications: {
29
+ create: InternalMutation;
30
+ verify: InternalMutation;
31
+ cleanup: InternalMutation;
32
+ };
33
+ };
34
+ lib: {
35
+ rateLimit: {
36
+ check: InternalQuery;
37
+ increment: InternalMutation;
38
+ reset: InternalMutation;
39
+ };
40
+ };
41
+ providers: {
42
+ emailPassword: {
43
+ signUp: InternalAction;
44
+ signIn: InternalAction;
45
+ verifyEmail: InternalAction;
46
+ requestPasswordReset: InternalAction;
47
+ resetPassword: InternalAction;
48
+ updatePasswordHash: InternalMutation;
49
+ };
50
+ oauth: {
51
+ getAuthorizationUrl: InternalAction;
52
+ handleCallback: InternalAction;
53
+ storeOAuthState: InternalMutation;
54
+ consumeOAuthState: InternalMutation;
55
+ };
56
+ };
57
+ plugins: {
58
+ admin: {
59
+ listUsers: InternalQuery;
60
+ banUser: InternalMutation;
61
+ unbanUser: InternalMutation;
62
+ setRole: InternalMutation;
63
+ deleteUser: InternalMutation;
64
+ };
65
+ };
66
+ };
@@ -0,0 +1,111 @@
1
+ import { v } from "convex/values";
2
+ import { internalMutation, internalQuery } from "../_generated/server";
3
+
4
+ /** Window duration: 10 minutes in milliseconds. */
5
+ const WINDOW_MS = 10 * 60 * 1000;
6
+
7
+ /** Max failures before lockout. */
8
+ const MAX_FAILURES = 10;
9
+
10
+ /** Lockout duration: 10 minutes. */
11
+ const LOCKOUT_MS = 10 * 60 * 1000;
12
+
13
+ /**
14
+ * Check if a key is currently rate limited.
15
+ * Returns { limited: true, retryAfter } if locked, { limited: false } otherwise.
16
+ */
17
+ export const check = internalQuery({
18
+ args: {
19
+ key: v.string(),
20
+ },
21
+ handler: async (ctx, { key }) => {
22
+ const now = Date.now();
23
+ const record = await ctx.db
24
+ .query("rateLimits")
25
+ .withIndex("by_key", (q) => q.eq("key", key))
26
+ .unique();
27
+
28
+ if (!record) {
29
+ return { limited: false as const };
30
+ }
31
+
32
+ // Check hard lockout
33
+ if (record.lockedUntil !== undefined && record.lockedUntil > now) {
34
+ return { limited: true as const, retryAfter: record.lockedUntil - now };
35
+ }
36
+
37
+ // Check sliding window count
38
+ const windowAge = now - record.windowStart;
39
+ if (windowAge < WINDOW_MS && record.count >= MAX_FAILURES) {
40
+ return { limited: true as const, retryAfter: WINDOW_MS - windowAge };
41
+ }
42
+
43
+ return { limited: false as const };
44
+ },
45
+ });
46
+
47
+ /**
48
+ * Increment failure count for a key. Applies lockout if threshold reached.
49
+ */
50
+ export const increment = internalMutation({
51
+ args: {
52
+ key: v.string(),
53
+ },
54
+ handler: async (ctx, { key }) => {
55
+ const now = Date.now();
56
+ const record = await ctx.db
57
+ .query("rateLimits")
58
+ .withIndex("by_key", (q) => q.eq("key", key))
59
+ .unique();
60
+
61
+ if (!record) {
62
+ await ctx.db.insert("rateLimits", {
63
+ key,
64
+ count: 1,
65
+ windowStart: now,
66
+ });
67
+ return;
68
+ }
69
+
70
+ const windowAge = now - record.windowStart;
71
+ let newCount: number;
72
+ let windowStart: number;
73
+
74
+ if (windowAge >= WINDOW_MS) {
75
+ // New window
76
+ newCount = 1;
77
+ windowStart = now;
78
+ } else {
79
+ newCount = record.count + 1;
80
+ windowStart = record.windowStart;
81
+ }
82
+
83
+ const lockedUntil =
84
+ newCount >= MAX_FAILURES ? now + LOCKOUT_MS : record.lockedUntil;
85
+
86
+ await ctx.db.patch(record._id, {
87
+ count: newCount,
88
+ windowStart,
89
+ lockedUntil,
90
+ });
91
+ },
92
+ });
93
+
94
+ /**
95
+ * Reset rate limit for a key (called on successful auth).
96
+ */
97
+ export const reset = internalMutation({
98
+ args: {
99
+ key: v.string(),
100
+ },
101
+ handler: async (ctx, { key }) => {
102
+ const record = await ctx.db
103
+ .query("rateLimits")
104
+ .withIndex("by_key", (q) => q.eq("key", key))
105
+ .unique();
106
+
107
+ if (record) {
108
+ await ctx.db.delete(record._id);
109
+ }
110
+ },
111
+ });
@@ -0,0 +1,12 @@
1
+ import { v } from "convex/values";
2
+
3
+ /** Runtime validator for OAuth provider configuration payloads. */
4
+ export const oauthProviderConfigValidator = v.object({
5
+ id: v.string(),
6
+ clientId: v.string(),
7
+ clientSecret: v.string(),
8
+ authorizationUrl: v.string(),
9
+ tokenUrl: v.string(),
10
+ userInfoUrl: v.string(),
11
+ scopes: v.array(v.string()),
12
+ });
@@ -0,0 +1,178 @@
1
+ import { v } from "convex/values";
2
+ import { internalMutation, internalQuery } from "../_generated/server";
3
+ import type { Id } from "../_generated/dataModel";
4
+
5
+ async function assertAdminActor(
6
+ ctx: { db: { get: (id: Id<"users">) => Promise<{ role?: string } | null> } },
7
+ actorUserId: Id<"users">
8
+ ): Promise<void> {
9
+ const actor = await ctx.db.get(actorUserId);
10
+ if (!actor) {
11
+ throw new Error("Unauthorized");
12
+ }
13
+ if (actor.role !== "admin") {
14
+ throw new Error("Forbidden");
15
+ }
16
+ }
17
+
18
+ /** List users with cursor-based pagination.
19
+ *
20
+ * paginate() is not available in components, so we use _creationTime as a
21
+ * cursor with .filter() + .take(). The cursor is a stringified _creationTime.
22
+ */
23
+ export const listUsers = internalQuery({
24
+ args: {
25
+ actorUserId: v.id("users"),
26
+ limit: v.optional(v.number()),
27
+ cursor: v.optional(v.string()),
28
+ },
29
+ handler: async (ctx, args) => {
30
+ await assertAdminActor(ctx, args.actorUserId);
31
+
32
+ const limit = args.limit ?? 20;
33
+
34
+ let query = ctx.db.query("users").order("asc");
35
+
36
+ if (args.cursor) {
37
+ const cursorTime = parseFloat(args.cursor);
38
+ query = query.filter((q) =>
39
+ q.gt(q.field("_creationTime"), cursorTime)
40
+ );
41
+ }
42
+
43
+ const rows = await query.take(limit + 1);
44
+ const hasMore = rows.length > limit;
45
+ const page = hasMore ? rows.slice(0, limit) : rows;
46
+ const last = page.at(-1);
47
+ const nextCursor =
48
+ hasMore && last
49
+ ? String(last._creationTime)
50
+ : null;
51
+
52
+ return {
53
+ users: page,
54
+ cursor: nextCursor,
55
+ isDone: !hasMore,
56
+ };
57
+ },
58
+ });
59
+
60
+ /** Get a single user by ID (admin view). */
61
+ export const getUser = internalQuery({
62
+ args: {
63
+ actorUserId: v.id("users"),
64
+ userId: v.id("users"),
65
+ },
66
+ handler: async (ctx, { actorUserId, userId }) => {
67
+ await assertAdminActor(ctx, actorUserId);
68
+ return await ctx.db.get(userId);
69
+ },
70
+ });
71
+
72
+ /**
73
+ * Ban a user. Sets banned=true, banReason, and optional banExpires.
74
+ * Also invalidates all active sessions.
75
+ */
76
+ export const banUser = internalMutation({
77
+ args: {
78
+ actorUserId: v.id("users"),
79
+ userId: v.id("users"),
80
+ reason: v.optional(v.string()),
81
+ expiresAt: v.optional(v.number()), // timestamp; undefined = permanent
82
+ },
83
+ handler: async (ctx, { actorUserId, userId, reason, expiresAt }) => {
84
+ await assertAdminActor(ctx, actorUserId);
85
+
86
+ const user = await ctx.db.get(userId);
87
+ if (!user) throw new Error("User not found");
88
+
89
+ await ctx.db.patch(userId, {
90
+ banned: true,
91
+ banReason: reason,
92
+ banExpires: expiresAt,
93
+ updatedAt: Date.now(),
94
+ });
95
+
96
+ // Invalidate all active sessions
97
+ const sessions = await ctx.db
98
+ .query("sessions")
99
+ .withIndex("by_userId", (q) => q.eq("userId", userId))
100
+ .collect();
101
+ for (const session of sessions) {
102
+ await ctx.db.delete(session._id);
103
+ }
104
+ },
105
+ });
106
+
107
+ /** Unban a user. Clears ban fields. */
108
+ export const unbanUser = internalMutation({
109
+ args: {
110
+ actorUserId: v.id("users"),
111
+ userId: v.id("users"),
112
+ },
113
+ handler: async (ctx, { actorUserId, userId }) => {
114
+ await assertAdminActor(ctx, actorUserId);
115
+
116
+ const user = await ctx.db.get(userId);
117
+ if (!user) throw new Error("User not found");
118
+
119
+ await ctx.db.patch(userId, {
120
+ banned: false,
121
+ banReason: undefined,
122
+ banExpires: undefined,
123
+ updatedAt: Date.now(),
124
+ });
125
+ },
126
+ });
127
+
128
+ /** Set a user's role. */
129
+ export const setRole = internalMutation({
130
+ args: {
131
+ actorUserId: v.id("users"),
132
+ userId: v.id("users"),
133
+ role: v.string(),
134
+ },
135
+ handler: async (ctx, { actorUserId, userId, role }) => {
136
+ await assertAdminActor(ctx, actorUserId);
137
+
138
+ const user = await ctx.db.get(userId);
139
+ if (!user) throw new Error("User not found");
140
+
141
+ await ctx.db.patch(userId, {
142
+ role,
143
+ updatedAt: Date.now(),
144
+ });
145
+ },
146
+ });
147
+
148
+ /** Delete a user and all associated data. */
149
+ export const deleteUser = internalMutation({
150
+ args: {
151
+ actorUserId: v.id("users"),
152
+ userId: v.id("users"),
153
+ },
154
+ handler: async (ctx, { actorUserId, userId }) => {
155
+ await assertAdminActor(ctx, actorUserId);
156
+
157
+ // Delete accounts
158
+ const accounts = await ctx.db
159
+ .query("accounts")
160
+ .withIndex("by_userId", (q) => q.eq("userId", userId))
161
+ .collect();
162
+ for (const account of accounts) {
163
+ await ctx.db.delete(account._id);
164
+ }
165
+
166
+ // Delete sessions
167
+ const sessions = await ctx.db
168
+ .query("sessions")
169
+ .withIndex("by_userId", (q) => q.eq("userId", userId))
170
+ .collect();
171
+ for (const session of sessions) {
172
+ await ctx.db.delete(session._id);
173
+ }
174
+
175
+ // Delete the user
176
+ await ctx.db.delete(userId);
177
+ },
178
+ });