@cortexmemory/cli 0.26.2 → 0.27.3

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 (90) hide show
  1. package/dist/commands/convex.js +1 -1
  2. package/dist/commands/convex.js.map +1 -1
  3. package/dist/commands/deploy.d.ts +1 -1
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +771 -144
  6. package/dist/commands/deploy.js.map +1 -1
  7. package/dist/commands/dev.d.ts.map +1 -1
  8. package/dist/commands/dev.js +210 -36
  9. package/dist/commands/dev.js.map +1 -1
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +273 -43
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/commands/setup.d.ts.map +1 -1
  14. package/dist/commands/setup.js +102 -46
  15. package/dist/commands/setup.js.map +1 -1
  16. package/dist/commands/status.d.ts.map +1 -1
  17. package/dist/commands/status.js +94 -7
  18. package/dist/commands/status.js.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/types.d.ts +23 -0
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/utils/app-template-sync.d.ts +95 -0
  23. package/dist/utils/app-template-sync.d.ts.map +1 -0
  24. package/dist/utils/app-template-sync.js +425 -0
  25. package/dist/utils/app-template-sync.js.map +1 -0
  26. package/dist/utils/config.d.ts +11 -0
  27. package/dist/utils/config.d.ts.map +1 -1
  28. package/dist/utils/config.js +20 -0
  29. package/dist/utils/config.js.map +1 -1
  30. package/dist/utils/deployment-selector.d.ts +21 -0
  31. package/dist/utils/deployment-selector.d.ts.map +1 -1
  32. package/dist/utils/deployment-selector.js +32 -0
  33. package/dist/utils/deployment-selector.js.map +1 -1
  34. package/dist/utils/init/graph-setup.d.ts.map +1 -1
  35. package/dist/utils/init/graph-setup.js +25 -2
  36. package/dist/utils/init/graph-setup.js.map +1 -1
  37. package/dist/utils/init/quickstart-setup.d.ts +87 -0
  38. package/dist/utils/init/quickstart-setup.d.ts.map +1 -0
  39. package/dist/utils/init/quickstart-setup.js +462 -0
  40. package/dist/utils/init/quickstart-setup.js.map +1 -0
  41. package/dist/utils/schema-sync.d.ts.map +1 -1
  42. package/dist/utils/schema-sync.js +27 -21
  43. package/dist/utils/schema-sync.js.map +1 -1
  44. package/package.json +3 -2
  45. package/templates/vercel-ai-quickstart/.env.local.example +45 -0
  46. package/templates/vercel-ai-quickstart/README.md +280 -0
  47. package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
  48. package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +83 -0
  49. package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
  50. package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
  51. package/templates/vercel-ai-quickstart/app/api/chat/route.ts +277 -0
  52. package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
  53. package/templates/vercel-ai-quickstart/app/api/facts/route.ts +39 -0
  54. package/templates/vercel-ai-quickstart/app/api/health/route.ts +99 -0
  55. package/templates/vercel-ai-quickstart/app/api/memories/route.ts +37 -0
  56. package/templates/vercel-ai-quickstart/app/globals.css +275 -0
  57. package/templates/vercel-ai-quickstart/app/layout.tsx +19 -0
  58. package/templates/vercel-ai-quickstart/app/page.tsx +216 -0
  59. package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
  60. package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
  61. package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
  62. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +334 -0
  63. package/templates/vercel-ai-quickstart/components/ConvexClientProvider.tsx +21 -0
  64. package/templates/vercel-ai-quickstart/components/DataPreview.tsx +57 -0
  65. package/templates/vercel-ai-quickstart/components/HealthStatus.tsx +214 -0
  66. package/templates/vercel-ai-quickstart/components/LayerCard.tsx +263 -0
  67. package/templates/vercel-ai-quickstart/components/LayerFlowDiagram.tsx +195 -0
  68. package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
  69. package/templates/vercel-ai-quickstart/components/MemorySpaceSwitcher.tsx +93 -0
  70. package/templates/vercel-ai-quickstart/convex/conversations.ts +67 -0
  71. package/templates/vercel-ai-quickstart/convex/facts.ts +131 -0
  72. package/templates/vercel-ai-quickstart/convex/health.ts +15 -0
  73. package/templates/vercel-ai-quickstart/convex/memories.ts +104 -0
  74. package/templates/vercel-ai-quickstart/convex/schema.ts +20 -0
  75. package/templates/vercel-ai-quickstart/convex/users.ts +105 -0
  76. package/templates/vercel-ai-quickstart/jest.config.js +45 -0
  77. package/templates/vercel-ai-quickstart/lib/animations.ts +146 -0
  78. package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
  79. package/templates/vercel-ai-quickstart/lib/layer-tracking.ts +214 -0
  80. package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
  81. package/templates/vercel-ai-quickstart/next.config.js +27 -0
  82. package/templates/vercel-ai-quickstart/package.json +46 -0
  83. package/templates/vercel-ai-quickstart/postcss.config.js +5 -0
  84. package/templates/vercel-ai-quickstart/tailwind.config.js +37 -0
  85. package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
  86. package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
  87. package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
  88. package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
  89. package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
  90. package/templates/vercel-ai-quickstart/tsconfig.json +33 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Password Hashing Utilities
3
+ *
4
+ * Uses Web Crypto API (PBKDF2) for password hashing.
5
+ * Works in Edge runtime - no external dependencies.
6
+ */
7
+
8
+ const ITERATIONS = 100000;
9
+ const KEY_LENGTH = 256;
10
+ const SALT_LENGTH = 16;
11
+
12
+ /**
13
+ * Hash a password using PBKDF2
14
+ *
15
+ * @param password - Plain text password to hash
16
+ * @returns Hashed password as base64 string (format: salt:hash)
17
+ */
18
+ export async function hashPassword(password: string): Promise<string> {
19
+ const encoder = new TextEncoder();
20
+ const passwordBuffer = encoder.encode(password);
21
+
22
+ // Generate random salt
23
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
24
+
25
+ // Import password as key material
26
+ const keyMaterial = await crypto.subtle.importKey(
27
+ "raw",
28
+ passwordBuffer,
29
+ "PBKDF2",
30
+ false,
31
+ ["deriveBits"]
32
+ );
33
+
34
+ // Derive key using PBKDF2
35
+ const derivedBits = await crypto.subtle.deriveBits(
36
+ {
37
+ name: "PBKDF2",
38
+ salt,
39
+ iterations: ITERATIONS,
40
+ hash: "SHA-256",
41
+ },
42
+ keyMaterial,
43
+ KEY_LENGTH
44
+ );
45
+
46
+ // Convert to base64 strings
47
+ const saltB64 = btoa(String.fromCharCode(...salt));
48
+ const hashB64 = btoa(String.fromCharCode(...new Uint8Array(derivedBits)));
49
+
50
+ // Return combined format: salt:hash
51
+ return `${saltB64}:${hashB64}`;
52
+ }
53
+
54
+ /**
55
+ * Verify a password against a stored hash
56
+ *
57
+ * @param password - Plain text password to verify
58
+ * @param storedHash - Previously hashed password (format: salt:hash)
59
+ * @returns True if password matches
60
+ */
61
+ export async function verifyPassword(
62
+ password: string,
63
+ storedHash: string
64
+ ): Promise<boolean> {
65
+ try {
66
+ const [saltB64, expectedHashB64] = storedHash.split(":");
67
+ if (!saltB64 || !expectedHashB64) {
68
+ return false;
69
+ }
70
+
71
+ const encoder = new TextEncoder();
72
+ const passwordBuffer = encoder.encode(password);
73
+
74
+ // Decode salt from base64
75
+ const saltStr = atob(saltB64);
76
+ const salt = new Uint8Array(saltStr.length);
77
+ for (let i = 0; i < saltStr.length; i++) {
78
+ salt[i] = saltStr.charCodeAt(i);
79
+ }
80
+
81
+ // Import password as key material
82
+ const keyMaterial = await crypto.subtle.importKey(
83
+ "raw",
84
+ passwordBuffer,
85
+ "PBKDF2",
86
+ false,
87
+ ["deriveBits"]
88
+ );
89
+
90
+ // Derive key using same parameters
91
+ const derivedBits = await crypto.subtle.deriveBits(
92
+ {
93
+ name: "PBKDF2",
94
+ salt,
95
+ iterations: ITERATIONS,
96
+ hash: "SHA-256",
97
+ },
98
+ keyMaterial,
99
+ KEY_LENGTH
100
+ );
101
+
102
+ // Compare hashes
103
+ const hashB64 = btoa(String.fromCharCode(...new Uint8Array(derivedBits)));
104
+ return hashB64 === expectedHashB64;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Generate a secure random session token
112
+ *
113
+ * @returns Random token as hex string
114
+ */
115
+ export function generateSessionToken(): string {
116
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
117
+ return Array.from(bytes)
118
+ .map((b) => b.toString(16).padStart(2, "0"))
119
+ .join("");
120
+ }
@@ -0,0 +1,27 @@
1
+ const path = require("path");
2
+
3
+ /** @type {import('next').NextConfig} */
4
+ const nextConfig = {
5
+ transpilePackages: ["@cortexmemory/sdk", "@cortexmemory/vercel-ai-provider"],
6
+ serverExternalPackages: ["convex"],
7
+ experimental: {
8
+ // Ensure linked packages resolve dependencies from this project's node_modules
9
+ externalDir: true,
10
+ },
11
+ // Empty turbopack config to silence the warning about missing turbopack config
12
+ turbopack: {},
13
+ // Webpack configuration for module resolution when SDK is file-linked
14
+ // This is needed because the SDK uses dynamic imports that don't resolve
15
+ // correctly from a linked package's location during local development
16
+ webpack: (config) => {
17
+ config.resolve.alias = {
18
+ ...config.resolve.alias,
19
+ "@anthropic-ai/sdk": path.resolve(__dirname, "node_modules/@anthropic-ai/sdk"),
20
+ "openai": path.resolve(__dirname, "node_modules/openai"),
21
+ "neo4j-driver": path.resolve(__dirname, "node_modules/neo4j-driver"),
22
+ };
23
+ return config;
24
+ },
25
+ };
26
+
27
+ module.exports = nextConfig;
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@cortexmemory/vercel-ai-quickstart",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "license": "UNLICENSED",
6
+ "description": "Cortex Memory + Vercel AI SDK Quickstart Demo",
7
+ "scripts": {
8
+ "dev": "next dev --webpack",
9
+ "build": "next build --webpack",
10
+ "start": "next start",
11
+ "lint": "next lint",
12
+ "test": "jest",
13
+ "test:watch": "jest --watch",
14
+ "convex:dev": "convex dev",
15
+ "convex:deploy": "convex deploy"
16
+ },
17
+ "dependencies": {
18
+ "@ai-sdk/openai": "^3.0.1",
19
+ "@ai-sdk/react": "^3.0.3",
20
+ "@anthropic-ai/sdk": "^0.71.2",
21
+ "@cortexmemory/sdk": "file:../../..",
22
+ "@cortexmemory/vercel-ai-provider": "file:..",
23
+ "ai": "^6.0.3",
24
+ "convex": "^1.31.2",
25
+ "framer-motion": "^12.23.26",
26
+ "neo4j-driver": "^6.0.1",
27
+ "next": "^16.1.1",
28
+ "openai": "^6.15.0",
29
+ "react": "^19.2.3",
30
+ "react-dom": "^19.2.3",
31
+ "zod": "^4.2.1"
32
+ },
33
+ "devDependencies": {
34
+ "@tailwindcss/postcss": "^4.1.18",
35
+ "@types/jest": "^29.5.14",
36
+ "@types/node": "^25.0.3",
37
+ "@types/react": "^19.2.7",
38
+ "@types/react-dom": "^19.2.3",
39
+ "autoprefixer": "^10.4.23",
40
+ "jest": "^29.7.0",
41
+ "postcss": "^8.5.6",
42
+ "tailwindcss": "^4.1.18",
43
+ "ts-jest": "^29.2.5",
44
+ "typescript": "^5.9.3"
45
+ }
46
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
@@ -0,0 +1,37 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
5
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ cortex: {
11
+ 50: "#f0f7ff",
12
+ 100: "#e0effe",
13
+ 200: "#b9dffc",
14
+ 300: "#7cc5fa",
15
+ 400: "#36a8f5",
16
+ 500: "#0c8ce6",
17
+ 600: "#006fc4",
18
+ 700: "#0159a0",
19
+ 800: "#064b84",
20
+ 900: "#0b3f6d",
21
+ 950: "#072848",
22
+ },
23
+ },
24
+ animation: {
25
+ "pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
26
+ flow: "flow 2s ease-in-out infinite",
27
+ },
28
+ keyframes: {
29
+ flow: {
30
+ "0%, 100%": { opacity: "0.3" },
31
+ "50%": { opacity: "1" },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ plugins: [],
37
+ };
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Mock Cortex SDK
3
+ *
4
+ * Provides a mock implementation of the Cortex SDK for testing.
5
+ * Simulates the SDK behavior without requiring a real Convex backend.
6
+ */
7
+
8
+ // In-memory stores for test state
9
+ let mutableStore: Map<string, Map<string, unknown>> = new Map();
10
+ let usersStore: Map<string, { userId: string; data: Record<string, unknown> }> =
11
+ new Map();
12
+ interface StoredConversation {
13
+ conversationId: string;
14
+ memorySpaceId: string;
15
+ type: string;
16
+ participants: { userId?: string; agentId?: string };
17
+ metadata: Record<string, unknown>;
18
+ createdAt: number;
19
+ updatedAt: number;
20
+ messageCount: number;
21
+ messages?: Array<{
22
+ id: string;
23
+ role: string;
24
+ content: string;
25
+ timestamp: number;
26
+ }>;
27
+ }
28
+
29
+ let conversationsStore: Map<string, StoredConversation> = new Map();
30
+
31
+ /**
32
+ * Reset all mock stores (call in beforeEach)
33
+ */
34
+ export function resetMockStores(): void {
35
+ mutableStore = new Map();
36
+ usersStore = new Map();
37
+ conversationsStore = new Map();
38
+ }
39
+
40
+ /**
41
+ * Create a mock Cortex SDK instance
42
+ */
43
+ export function createMockCortex() {
44
+ return {
45
+ mutable: {
46
+ get: jest.fn(
47
+ async (namespace: string, key: string): Promise<unknown | null> => {
48
+ const ns = mutableStore.get(namespace);
49
+ if (!ns) return null;
50
+ return ns.get(key) ?? null;
51
+ }
52
+ ),
53
+ set: jest.fn(
54
+ async (
55
+ namespace: string,
56
+ key: string,
57
+ value: unknown
58
+ ): Promise<void> => {
59
+ if (!mutableStore.has(namespace)) {
60
+ mutableStore.set(namespace, new Map());
61
+ }
62
+ mutableStore.get(namespace)!.set(key, value);
63
+ }
64
+ ),
65
+ delete: jest.fn(
66
+ async (namespace: string, key: string): Promise<void> => {
67
+ const ns = mutableStore.get(namespace);
68
+ if (ns) ns.delete(key);
69
+ }
70
+ ),
71
+ },
72
+ users: {
73
+ get: jest.fn(
74
+ async (
75
+ userId: string
76
+ ): Promise<{
77
+ userId: string;
78
+ data: Record<string, unknown>;
79
+ } | null> => {
80
+ return usersStore.get(userId) ?? null;
81
+ }
82
+ ),
83
+ update: jest.fn(
84
+ async (
85
+ userId: string,
86
+ data: Record<string, unknown>
87
+ ): Promise<{ userId: string; data: Record<string, unknown> }> => {
88
+ const existing = usersStore.get(userId);
89
+ const user = {
90
+ userId,
91
+ data: { ...(existing?.data || {}), ...data },
92
+ };
93
+ usersStore.set(userId, user);
94
+ return user;
95
+ }
96
+ ),
97
+ delete: jest.fn(async (userId: string): Promise<void> => {
98
+ usersStore.delete(userId);
99
+ }),
100
+ list: jest.fn(async () => {
101
+ return {
102
+ users: Array.from(usersStore.values()),
103
+ hasMore: false,
104
+ };
105
+ }),
106
+ },
107
+ conversations: {
108
+ list: jest.fn(
109
+ async (params: { memorySpaceId?: string; userId?: string }) => {
110
+ const conversations = Array.from(conversationsStore.values()).filter(
111
+ (conv) => {
112
+ if (
113
+ params.memorySpaceId &&
114
+ conv.memorySpaceId !== params.memorySpaceId
115
+ ) {
116
+ return false;
117
+ }
118
+ if (
119
+ params.userId &&
120
+ conv.participants.userId !== params.userId
121
+ ) {
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ );
127
+ return { conversations, hasMore: false };
128
+ }
129
+ ),
130
+ create: jest.fn(
131
+ async (params: {
132
+ memorySpaceId: string;
133
+ conversationId: string;
134
+ type: string;
135
+ participants: { userId?: string; agentId?: string };
136
+ metadata?: Record<string, unknown>;
137
+ }) => {
138
+ const now = Date.now();
139
+ const conversation = {
140
+ conversationId: params.conversationId,
141
+ memorySpaceId: params.memorySpaceId,
142
+ type: params.type,
143
+ participants: params.participants,
144
+ metadata: params.metadata || {},
145
+ createdAt: now,
146
+ updatedAt: now,
147
+ messageCount: 0,
148
+ };
149
+ conversationsStore.set(params.conversationId, conversation);
150
+ return conversation;
151
+ }
152
+ ),
153
+ get: jest.fn(async (conversationId: string, options?: { includeMessages?: boolean; messageLimit?: number }) => {
154
+ const conv = conversationsStore.get(conversationId);
155
+ if (!conv) return null;
156
+
157
+ // If includeMessages is requested, return with messages
158
+ if (options?.includeMessages) {
159
+ return {
160
+ ...conv,
161
+ messages: conv.messages || [],
162
+ };
163
+ }
164
+
165
+ // Otherwise return without messages
166
+ const { messages: _messages, ...convWithoutMessages } = conv;
167
+ return convWithoutMessages;
168
+ }),
169
+ delete: jest.fn(async (conversationId: string): Promise<void> => {
170
+ conversationsStore.delete(conversationId);
171
+ }),
172
+ update: jest.fn(
173
+ async (
174
+ conversationId: string,
175
+ updates: { metadata?: Record<string, unknown> }
176
+ ) => {
177
+ const conv = conversationsStore.get(conversationId);
178
+ if (!conv) throw new Error("Conversation not found");
179
+ const updated = {
180
+ ...conv,
181
+ metadata: { ...conv.metadata, ...updates.metadata },
182
+ updatedAt: Date.now(),
183
+ };
184
+ conversationsStore.set(conversationId, updated);
185
+ return updated;
186
+ }
187
+ ),
188
+ },
189
+ memory: {
190
+ search: jest.fn().mockResolvedValue([]),
191
+ remember: jest.fn().mockResolvedValue({
192
+ conversation: { conversationId: "conv-1", messageIds: [] },
193
+ memories: [],
194
+ facts: [],
195
+ }),
196
+ recall: jest.fn().mockResolvedValue({
197
+ context: "",
198
+ totalResults: 0,
199
+ queryTimeMs: 10,
200
+ sources: { vector: { count: 0, items: [] } },
201
+ }),
202
+ },
203
+ close: jest.fn(),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Type for the mock Cortex instance
209
+ */
210
+ export type MockCortex = ReturnType<typeof createMockCortex>;
211
+
212
+ /**
213
+ * Helper to seed test data into stores
214
+ */
215
+ export const seedTestData = {
216
+ user: (
217
+ userId: string,
218
+ data: Record<string, unknown> = {}
219
+ ): { userId: string; data: Record<string, unknown> } => {
220
+ const user = { userId, data };
221
+ usersStore.set(userId, user);
222
+ return user;
223
+ },
224
+ mutable: (namespace: string, key: string, value: unknown): void => {
225
+ if (!mutableStore.has(namespace)) {
226
+ mutableStore.set(namespace, new Map());
227
+ }
228
+ mutableStore.get(namespace)!.set(key, value);
229
+ },
230
+ conversation: (
231
+ conversationId: string,
232
+ params: {
233
+ memorySpaceId?: string;
234
+ userId?: string;
235
+ title?: string;
236
+ messages?: Array<{ role: string; content: string }>;
237
+ } = {}
238
+ ) => {
239
+ const now = Date.now();
240
+ const messages = params.messages?.map((msg, i) => ({
241
+ id: `msg-${i}`,
242
+ role: msg.role,
243
+ content: msg.content,
244
+ timestamp: now + i,
245
+ }));
246
+ const conversation: StoredConversation = {
247
+ conversationId,
248
+ memorySpaceId: params.memorySpaceId || "quickstart-demo",
249
+ type: "user-agent",
250
+ participants: {
251
+ userId: params.userId || "test-user",
252
+ agentId: "quickstart-assistant",
253
+ },
254
+ metadata: { title: params.title || "Test Chat" },
255
+ createdAt: now,
256
+ updatedAt: now,
257
+ messageCount: messages?.length || 0,
258
+ messages,
259
+ };
260
+ conversationsStore.set(conversationId, conversation);
261
+ return conversation;
262
+ },
263
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Jest Test Setup
3
+ *
4
+ * Configures global test environment for quickstart tests.
5
+ */
6
+
7
+ import { TextEncoder, TextDecoder } from "util";
8
+
9
+ // Polyfill TextEncoder/TextDecoder for Node.js environment
10
+ global.TextEncoder = TextEncoder;
11
+ global.TextDecoder = TextDecoder as typeof global.TextDecoder;
12
+
13
+ // Polyfill crypto for Node.js environment (required for password utilities)
14
+ if (typeof global.crypto === "undefined") {
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const { webcrypto } = require("crypto");
17
+ global.crypto = webcrypto;
18
+ }
19
+
20
+ // Polyfill btoa/atob for Node.js environment
21
+ if (typeof global.btoa === "undefined") {
22
+ global.btoa = (str: string) => Buffer.from(str, "binary").toString("base64");
23
+ }
24
+
25
+ if (typeof global.atob === "undefined") {
26
+ global.atob = (str: string) => Buffer.from(str, "base64").toString("binary");
27
+ }
28
+
29
+ // Set test environment variables
30
+ process.env.NODE_ENV = "test";
31
+ process.env.CONVEX_URL = "https://test.convex.cloud";
32
+
33
+ // Suppress console output during tests (optional - comment out for debugging)
34
+ // global.console = {
35
+ // ...console,
36
+ // log: jest.fn(),
37
+ // debug: jest.fn(),
38
+ // info: jest.fn(),
39
+ // warn: jest.fn(),
40
+ // };
41
+
42
+ // Increase test timeout for async operations
43
+ jest.setTimeout(10000);
44
+
45
+ // Reset mocks between tests
46
+ beforeEach(() => {
47
+ jest.clearAllMocks();
48
+ });