@cortexmemory/cli 0.27.3 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +18 -6
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +191 -80
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.js +3 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/app-template-sync.d.ts.map +1 -1
- package/dist/utils/app-template-sync.js +35 -13
- package/dist/utils/app-template-sync.js.map +1 -1
- package/dist/utils/init/quickstart-setup.d.ts.map +1 -1
- package/dist/utils/init/quickstart-setup.js.map +1 -1
- package/package.json +4 -4
- package/templates/basic/.env.local.example +23 -0
- package/templates/basic/README.md +181 -56
- package/templates/basic/package-lock.json +2180 -406
- package/templates/basic/package.json +23 -5
- package/templates/basic/src/__tests__/chat.test.ts +340 -0
- package/templates/basic/src/__tests__/cortex.test.ts +260 -0
- package/templates/basic/src/__tests__/display.test.ts +455 -0
- package/templates/basic/src/__tests__/e2e/fact-extraction.test.ts +498 -0
- package/templates/basic/src/__tests__/e2e/memory-flow.test.ts +355 -0
- package/templates/basic/src/__tests__/e2e/server-e2e.test.ts +414 -0
- package/templates/basic/src/__tests__/helpers/test-utils.ts +345 -0
- package/templates/basic/src/__tests__/integration/chat-flow.test.ts +422 -0
- package/templates/basic/src/__tests__/integration/server.test.ts +441 -0
- package/templates/basic/src/__tests__/llm.test.ts +344 -0
- package/templates/basic/src/chat.ts +300 -0
- package/templates/basic/src/cortex.ts +203 -0
- package/templates/basic/src/display.ts +425 -0
- package/templates/basic/src/index.ts +194 -64
- package/templates/basic/src/llm.ts +214 -0
- package/templates/basic/src/server.ts +280 -0
- package/templates/basic/vitest.config.ts +33 -0
- package/templates/basic/vitest.e2e.config.ts +28 -0
- package/templates/basic/vitest.integration.config.ts +25 -0
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +1 -1
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +61 -19
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +14 -18
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +4 -7
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +95 -23
- package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +339 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +16 -16
- package/templates/vercel-ai-quickstart/app/globals.css +24 -9
- package/templates/vercel-ai-quickstart/app/page.tsx +41 -15
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +3 -1
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +6 -6
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +19 -8
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +46 -16
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +10 -5
- package/templates/vercel-ai-quickstart/jest.config.js +8 -1
- package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +5 -5
- package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
- package/templates/vercel-ai-quickstart/next.config.js +10 -2
- package/templates/vercel-ai-quickstart/package.json +23 -12
- package/templates/vercel-ai-quickstart/test-api.mjs +303 -0
- package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +483 -0
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +40 -40
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +8 -8
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +12 -8
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +4 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cortex Memory - HTTP Server Mode
|
|
3
|
+
*
|
|
4
|
+
* REST API server for demonstrating Cortex Memory SDK.
|
|
5
|
+
* Useful for testing with tools like curl, Postman, or integrating with other apps.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npm run server
|
|
9
|
+
*
|
|
10
|
+
* Endpoints:
|
|
11
|
+
* POST /chat Chat and store memory
|
|
12
|
+
* GET /recall Search memories
|
|
13
|
+
* GET /facts List stored facts
|
|
14
|
+
* GET /history/:id Get conversation history
|
|
15
|
+
* GET /health Health check
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { serve } from "@hono/node-server";
|
|
19
|
+
import { Hono } from "hono";
|
|
20
|
+
import { cors } from "hono/cors";
|
|
21
|
+
import { logger } from "hono/logger";
|
|
22
|
+
import { closeCortex, CONFIG } from "./cortex.js";
|
|
23
|
+
import {
|
|
24
|
+
chat,
|
|
25
|
+
recallMemories,
|
|
26
|
+
listFacts,
|
|
27
|
+
generateConversationId,
|
|
28
|
+
} from "./chat.js";
|
|
29
|
+
import { printWelcome, printInfo, printError } from "./display.js";
|
|
30
|
+
|
|
31
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
32
|
+
// Server Setup
|
|
33
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
+
|
|
35
|
+
const app = new Hono();
|
|
36
|
+
|
|
37
|
+
// Middleware
|
|
38
|
+
app.use("*", cors());
|
|
39
|
+
app.use("*", logger());
|
|
40
|
+
|
|
41
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
// Routes
|
|
43
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Health check
|
|
47
|
+
*/
|
|
48
|
+
app.get("/health", (c) => {
|
|
49
|
+
return c.json({
|
|
50
|
+
status: "ok",
|
|
51
|
+
memorySpaceId: CONFIG.memorySpaceId,
|
|
52
|
+
agentId: CONFIG.agentId,
|
|
53
|
+
features: {
|
|
54
|
+
factExtraction: CONFIG.enableFactExtraction,
|
|
55
|
+
graphSync: CONFIG.enableGraphMemory,
|
|
56
|
+
llm: !!process.env.OPENAI_API_KEY,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Chat endpoint
|
|
63
|
+
*
|
|
64
|
+
* POST /chat
|
|
65
|
+
* Body: { message: string, conversationId?: string }
|
|
66
|
+
*/
|
|
67
|
+
app.post("/chat", async (c) => {
|
|
68
|
+
try {
|
|
69
|
+
const body = await c.req.json();
|
|
70
|
+
const { message, conversationId } = body;
|
|
71
|
+
|
|
72
|
+
if (!message || typeof message !== "string") {
|
|
73
|
+
return c.json({ error: "message is required" }, 400);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const convId = conversationId || generateConversationId();
|
|
77
|
+
|
|
78
|
+
console.log(`\n[Chat] User: ${message.slice(0, 50)}${message.length > 50 ? "..." : ""}`);
|
|
79
|
+
|
|
80
|
+
const result = await chat(message, convId);
|
|
81
|
+
|
|
82
|
+
console.log(`[Chat] Response sent (${result.memoriesRecalled} memories, ${result.factsRecalled} facts recalled)\n`);
|
|
83
|
+
|
|
84
|
+
return c.json({
|
|
85
|
+
response: result.response,
|
|
86
|
+
conversationId: result.conversationId,
|
|
87
|
+
memoriesRecalled: result.memoriesRecalled,
|
|
88
|
+
factsRecalled: result.factsRecalled,
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
92
|
+
printError("Chat failed", error instanceof Error ? error : undefined);
|
|
93
|
+
return c.json({ error: message }, 500);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Recall endpoint
|
|
99
|
+
*
|
|
100
|
+
* GET /recall?query=<query>
|
|
101
|
+
*/
|
|
102
|
+
app.get("/recall", async (c) => {
|
|
103
|
+
try {
|
|
104
|
+
const query = c.req.query("query");
|
|
105
|
+
|
|
106
|
+
if (!query) {
|
|
107
|
+
return c.json({ error: "query parameter is required" }, 400);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`\n[Recall] Query: ${query}`);
|
|
111
|
+
|
|
112
|
+
// Use the internal recall function but capture results
|
|
113
|
+
const { getCortex } = await import("./cortex.js");
|
|
114
|
+
const cortex = getCortex();
|
|
115
|
+
|
|
116
|
+
const result = await cortex.memory.recall({
|
|
117
|
+
memorySpaceId: CONFIG.memorySpaceId,
|
|
118
|
+
query,
|
|
119
|
+
limit: 10,
|
|
120
|
+
sources: {
|
|
121
|
+
vector: true,
|
|
122
|
+
facts: true,
|
|
123
|
+
graph: CONFIG.enableGraphMemory,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
console.log(`[Recall] Found ${result.memories?.length || 0} memories, ${result.facts?.length || 0} facts\n`);
|
|
128
|
+
|
|
129
|
+
return c.json({
|
|
130
|
+
memories: result.memories || [],
|
|
131
|
+
facts: result.facts || [],
|
|
132
|
+
query,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
136
|
+
return c.json({ error: message }, 500);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Facts endpoint
|
|
142
|
+
*
|
|
143
|
+
* GET /facts
|
|
144
|
+
*/
|
|
145
|
+
app.get("/facts", async (c) => {
|
|
146
|
+
try {
|
|
147
|
+
const { getCortex } = await import("./cortex.js");
|
|
148
|
+
const cortex = getCortex();
|
|
149
|
+
|
|
150
|
+
const result = await cortex.facts.list({
|
|
151
|
+
memorySpaceId: CONFIG.memorySpaceId,
|
|
152
|
+
limit: 50,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const facts = result.facts || result || [];
|
|
156
|
+
|
|
157
|
+
return c.json({ facts, count: facts.length });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
160
|
+
return c.json({ error: message }, 500);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Conversation history endpoint
|
|
166
|
+
*
|
|
167
|
+
* GET /history/:conversationId
|
|
168
|
+
*/
|
|
169
|
+
app.get("/history/:conversationId", async (c) => {
|
|
170
|
+
try {
|
|
171
|
+
const conversationId = c.req.param("conversationId");
|
|
172
|
+
|
|
173
|
+
const { getCortex } = await import("./cortex.js");
|
|
174
|
+
const cortex = getCortex();
|
|
175
|
+
|
|
176
|
+
const conversation = await cortex.conversations.get(conversationId);
|
|
177
|
+
|
|
178
|
+
if (!conversation) {
|
|
179
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return c.json({
|
|
183
|
+
conversationId,
|
|
184
|
+
messages: conversation.messages || [],
|
|
185
|
+
messageCount: conversation.messages?.length || 0,
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
189
|
+
return c.json({ error: message }, 500);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Root endpoint - API docs
|
|
195
|
+
*/
|
|
196
|
+
app.get("/", (c) => {
|
|
197
|
+
return c.json({
|
|
198
|
+
name: "Cortex Memory - Basic Demo API",
|
|
199
|
+
version: "1.0.0",
|
|
200
|
+
endpoints: {
|
|
201
|
+
"POST /chat": {
|
|
202
|
+
description: "Chat and store memory",
|
|
203
|
+
body: { message: "string", conversationId: "string (optional)" },
|
|
204
|
+
},
|
|
205
|
+
"GET /recall": {
|
|
206
|
+
description: "Search memories",
|
|
207
|
+
query: { query: "string" },
|
|
208
|
+
},
|
|
209
|
+
"GET /facts": {
|
|
210
|
+
description: "List all stored facts",
|
|
211
|
+
},
|
|
212
|
+
"GET /history/:id": {
|
|
213
|
+
description: "Get conversation history",
|
|
214
|
+
},
|
|
215
|
+
"GET /health": {
|
|
216
|
+
description: "Health check",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
// Server Start
|
|
224
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
225
|
+
|
|
226
|
+
const PORT = parseInt(process.env.PORT || "3001", 10);
|
|
227
|
+
|
|
228
|
+
async function main(): Promise<void> {
|
|
229
|
+
// Check for required environment
|
|
230
|
+
if (!process.env.CONVEX_URL) {
|
|
231
|
+
printError(
|
|
232
|
+
"CONVEX_URL is required. Set it in .env.local or run: cortex init",
|
|
233
|
+
);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Print welcome
|
|
238
|
+
printWelcome("server");
|
|
239
|
+
|
|
240
|
+
// Start server
|
|
241
|
+
console.log(`🚀 Server starting on http://localhost:${PORT}`);
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log("Endpoints:");
|
|
244
|
+
console.log(` POST http://localhost:${PORT}/chat`);
|
|
245
|
+
console.log(` GET http://localhost:${PORT}/recall?query=...`);
|
|
246
|
+
console.log(` GET http://localhost:${PORT}/facts`);
|
|
247
|
+
console.log(` GET http://localhost:${PORT}/history/:id`);
|
|
248
|
+
console.log(` GET http://localhost:${PORT}/health`);
|
|
249
|
+
console.log("");
|
|
250
|
+
console.log("Example:");
|
|
251
|
+
console.log(` curl -X POST http://localhost:${PORT}/chat \\`);
|
|
252
|
+
console.log(' -H "Content-Type: application/json" \\');
|
|
253
|
+
console.log(' -d \'{"message": "My name is Alex"}\'');
|
|
254
|
+
console.log("");
|
|
255
|
+
|
|
256
|
+
serve({
|
|
257
|
+
fetch: app.fetch,
|
|
258
|
+
port: PORT,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
printInfo(`Server running on port ${PORT}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Handle cleanup
|
|
265
|
+
process.on("SIGINT", () => {
|
|
266
|
+
console.log("\n\nShutting down...");
|
|
267
|
+
closeCortex();
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
process.on("SIGTERM", () => {
|
|
272
|
+
closeCortex();
|
|
273
|
+
process.exit(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Run
|
|
277
|
+
main().catch((error) => {
|
|
278
|
+
printError("Fatal error", error instanceof Error ? error : undefined);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
include: [
|
|
8
|
+
"src/**/*.test.ts",
|
|
9
|
+
"src/__tests__/**/*.test.ts",
|
|
10
|
+
],
|
|
11
|
+
// Unit tests run fast
|
|
12
|
+
testTimeout: 10000,
|
|
13
|
+
coverage: {
|
|
14
|
+
provider: "v8",
|
|
15
|
+
reporter: ["text", "json", "html"],
|
|
16
|
+
include: ["src/**/*.ts"],
|
|
17
|
+
exclude: [
|
|
18
|
+
"src/**/*.test.ts",
|
|
19
|
+
"src/__tests__/**",
|
|
20
|
+
"src/index.ts", // Entry point, tested via integration
|
|
21
|
+
"src/server.ts", // Entry point, tested via integration
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
clearMocks: true,
|
|
25
|
+
restoreMocks: true,
|
|
26
|
+
// Separate pools for different test types
|
|
27
|
+
poolOptions: {
|
|
28
|
+
forks: {
|
|
29
|
+
singleFork: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for E2E tests.
|
|
5
|
+
* These tests require real Convex backend and optionally OpenAI API key.
|
|
6
|
+
*
|
|
7
|
+
* Run with:
|
|
8
|
+
* CONVEX_URL=<url> npm run test:e2e
|
|
9
|
+
* CONVEX_URL=<url> OPENAI_API_KEY=<key> npm run test:e2e
|
|
10
|
+
*/
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
test: {
|
|
13
|
+
globals: true,
|
|
14
|
+
environment: "node",
|
|
15
|
+
include: ["src/__tests__/e2e/**/*.test.ts"],
|
|
16
|
+
testTimeout: 180000, // 3 minutes for E2E tests (fact extraction takes time)
|
|
17
|
+
hookTimeout: 60000, // 1 minute for setup/teardown
|
|
18
|
+
clearMocks: true,
|
|
19
|
+
restoreMocks: true,
|
|
20
|
+
// Run E2E tests sequentially to avoid rate limits
|
|
21
|
+
pool: "forks",
|
|
22
|
+
poolOptions: {
|
|
23
|
+
forks: {
|
|
24
|
+
singleFork: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for integration tests.
|
|
5
|
+
* These tests use mocked SDK but test real component interactions.
|
|
6
|
+
*/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: "node",
|
|
11
|
+
include: ["src/__tests__/integration/**/*.test.ts"],
|
|
12
|
+
testTimeout: 30000, // 30 seconds for integration tests
|
|
13
|
+
coverage: {
|
|
14
|
+
provider: "v8",
|
|
15
|
+
reporter: ["text", "json", "html"],
|
|
16
|
+
include: ["src/**/*.ts"],
|
|
17
|
+
exclude: [
|
|
18
|
+
"src/**/*.test.ts",
|
|
19
|
+
"src/__tests__/**",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
clearMocks: true,
|
|
23
|
+
restoreMocks: true,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -7,25 +7,69 @@
|
|
|
7
7
|
import { getCortex } from "@/lib/cortex";
|
|
8
8
|
import { verifyPassword, generateSessionToken } from "@/lib/password";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Validates login request body structure.
|
|
12
|
+
* Returns validated credentials or null if invalid.
|
|
13
|
+
*/
|
|
14
|
+
function validateLoginBody(
|
|
15
|
+
body: unknown,
|
|
16
|
+
): { username: string; password: string } | null {
|
|
17
|
+
if (typeof body !== "object" || body === null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const record = body as Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
// Validate username field exists and is a non-empty string
|
|
24
|
+
const hasValidUsername =
|
|
25
|
+
"username" in record &&
|
|
26
|
+
typeof record.username === "string" &&
|
|
27
|
+
record.username.length > 0 &&
|
|
28
|
+
record.username.length <= 256;
|
|
29
|
+
|
|
30
|
+
// Validate password field exists and is a non-empty string
|
|
31
|
+
const hasValidPassword =
|
|
32
|
+
"password" in record &&
|
|
33
|
+
typeof record.password === "string" &&
|
|
34
|
+
record.password.length > 0 &&
|
|
35
|
+
record.password.length <= 1024;
|
|
36
|
+
|
|
37
|
+
if (!hasValidUsername || !hasValidPassword) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
username: record.username as string,
|
|
43
|
+
password: record.password as string,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Safely extracts an error message for logging without exposing user data.
|
|
49
|
+
*/
|
|
50
|
+
function getSafeErrorMessage(error: unknown): string {
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
// Only include error name and a sanitized message
|
|
53
|
+
// Avoid logging full stack traces which may contain user data
|
|
54
|
+
return `${error.name}: ${error.message.slice(0, 200)}`;
|
|
55
|
+
}
|
|
56
|
+
return "Unknown error";
|
|
57
|
+
}
|
|
58
|
+
|
|
10
59
|
export async function POST(req: Request) {
|
|
11
60
|
try {
|
|
12
61
|
const body = await req.json();
|
|
13
|
-
const { username, password } = body;
|
|
14
62
|
|
|
15
|
-
// Validate input
|
|
16
|
-
|
|
63
|
+
// Validate input structure before extracting values
|
|
64
|
+
const credentials = validateLoginBody(body);
|
|
65
|
+
if (!credentials) {
|
|
17
66
|
return Response.json(
|
|
18
|
-
{ error: "Username
|
|
19
|
-
{ status: 400 }
|
|
67
|
+
{ error: "Username and password are required" },
|
|
68
|
+
{ status: 400 },
|
|
20
69
|
);
|
|
21
70
|
}
|
|
22
71
|
|
|
23
|
-
|
|
24
|
-
return Response.json(
|
|
25
|
-
{ error: "Password is required" },
|
|
26
|
-
{ status: 400 }
|
|
27
|
-
);
|
|
28
|
-
}
|
|
72
|
+
const { username, password } = credentials;
|
|
29
73
|
|
|
30
74
|
const cortex = getCortex();
|
|
31
75
|
const sanitizedUsername = username.toLowerCase();
|
|
@@ -35,7 +79,7 @@ export async function POST(req: Request) {
|
|
|
35
79
|
if (!user) {
|
|
36
80
|
return Response.json(
|
|
37
81
|
{ error: "Invalid username or password" },
|
|
38
|
-
{ status: 401 }
|
|
82
|
+
{ status: 401 },
|
|
39
83
|
);
|
|
40
84
|
}
|
|
41
85
|
|
|
@@ -44,7 +88,7 @@ export async function POST(req: Request) {
|
|
|
44
88
|
if (!storedHash) {
|
|
45
89
|
return Response.json(
|
|
46
90
|
{ error: "Invalid username or password" },
|
|
47
|
-
{ status: 401 }
|
|
91
|
+
{ status: 401 },
|
|
48
92
|
);
|
|
49
93
|
}
|
|
50
94
|
|
|
@@ -52,7 +96,7 @@ export async function POST(req: Request) {
|
|
|
52
96
|
if (!isValid) {
|
|
53
97
|
return Response.json(
|
|
54
98
|
{ error: "Invalid username or password" },
|
|
55
|
-
{ status: 401 }
|
|
99
|
+
{ status: 401 },
|
|
56
100
|
);
|
|
57
101
|
}
|
|
58
102
|
|
|
@@ -73,11 +117,9 @@ export async function POST(req: Request) {
|
|
|
73
117
|
sessionToken,
|
|
74
118
|
});
|
|
75
119
|
} catch (error) {
|
|
76
|
-
|
|
120
|
+
// Log sanitized error to prevent log injection
|
|
121
|
+
console.error("[Login Error]", getSafeErrorMessage(error));
|
|
77
122
|
|
|
78
|
-
return Response.json(
|
|
79
|
-
{ error: "Failed to authenticate" },
|
|
80
|
-
{ status: 500 }
|
|
81
|
-
);
|
|
123
|
+
return Response.json({ error: "Failed to authenticate" }, { status: 500 });
|
|
82
124
|
}
|
|
83
125
|
}
|
|
@@ -14,39 +14,38 @@ export async function POST(req: Request) {
|
|
|
14
14
|
|
|
15
15
|
// Validate input
|
|
16
16
|
if (!username || typeof username !== "string") {
|
|
17
|
-
return Response.json(
|
|
18
|
-
{ error: "Username is required" },
|
|
19
|
-
{ status: 400 }
|
|
20
|
-
);
|
|
17
|
+
return Response.json({ error: "Username is required" }, { status: 400 });
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
if (!password || typeof password !== "string") {
|
|
24
|
-
return Response.json(
|
|
25
|
-
{ error: "Password is required" },
|
|
26
|
-
{ status: 400 }
|
|
27
|
-
);
|
|
21
|
+
return Response.json({ error: "Password is required" }, { status: 400 });
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
if (username.length < 2) {
|
|
31
25
|
return Response.json(
|
|
32
26
|
{ error: "Username must be at least 2 characters" },
|
|
33
|
-
{ status: 400 }
|
|
27
|
+
{ status: 400 },
|
|
34
28
|
);
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
if (password.length < 4) {
|
|
38
32
|
return Response.json(
|
|
39
33
|
{ error: "Password must be at least 4 characters" },
|
|
40
|
-
{ status: 400 }
|
|
34
|
+
{ status: 400 },
|
|
41
35
|
);
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
// Sanitize username (alphanumeric, underscore, hyphen only)
|
|
45
|
-
const sanitizedUsername = username
|
|
39
|
+
const sanitizedUsername = username
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9_-]/g, "");
|
|
46
42
|
if (sanitizedUsername !== username.toLowerCase()) {
|
|
47
43
|
return Response.json(
|
|
48
|
-
{
|
|
49
|
-
|
|
44
|
+
{
|
|
45
|
+
error:
|
|
46
|
+
"Username can only contain letters, numbers, underscores, and hyphens",
|
|
47
|
+
},
|
|
48
|
+
{ status: 400 },
|
|
50
49
|
);
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -57,7 +56,7 @@ export async function POST(req: Request) {
|
|
|
57
56
|
if (existingUser) {
|
|
58
57
|
return Response.json(
|
|
59
58
|
{ error: "Username already taken" },
|
|
60
|
-
{ status: 409 }
|
|
59
|
+
{ status: 409 },
|
|
61
60
|
);
|
|
62
61
|
}
|
|
63
62
|
|
|
@@ -86,9 +85,6 @@ export async function POST(req: Request) {
|
|
|
86
85
|
} catch (error) {
|
|
87
86
|
console.error("[Register Error]", error);
|
|
88
87
|
|
|
89
|
-
return Response.json(
|
|
90
|
-
{ error: "Failed to register user" },
|
|
91
|
-
{ status: 500 }
|
|
92
|
-
);
|
|
88
|
+
return Response.json({ error: "Failed to register user" }, { status: 500 });
|
|
93
89
|
}
|
|
94
90
|
}
|
|
@@ -16,16 +16,13 @@ export async function POST(req: Request) {
|
|
|
16
16
|
const { password } = body;
|
|
17
17
|
|
|
18
18
|
if (!password || typeof password !== "string") {
|
|
19
|
-
return Response.json(
|
|
20
|
-
{ error: "Password is required" },
|
|
21
|
-
{ status: 400 }
|
|
22
|
-
);
|
|
19
|
+
return Response.json({ error: "Password is required" }, { status: 400 });
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
if (password.length < 4) {
|
|
26
23
|
return Response.json(
|
|
27
24
|
{ error: "Password must be at least 4 characters" },
|
|
28
|
-
{ status: 400 }
|
|
25
|
+
{ status: 400 },
|
|
29
26
|
);
|
|
30
27
|
}
|
|
31
28
|
|
|
@@ -36,7 +33,7 @@ export async function POST(req: Request) {
|
|
|
36
33
|
if (existingHash !== null) {
|
|
37
34
|
return Response.json(
|
|
38
35
|
{ error: "Admin already configured" },
|
|
39
|
-
{ status: 409 }
|
|
36
|
+
{ status: 409 },
|
|
40
37
|
);
|
|
41
38
|
}
|
|
42
39
|
|
|
@@ -53,7 +50,7 @@ export async function POST(req: Request) {
|
|
|
53
50
|
|
|
54
51
|
return Response.json(
|
|
55
52
|
{ error: "Failed to configure admin password" },
|
|
56
|
-
{ status: 500 }
|
|
53
|
+
{ status: 500 },
|
|
57
54
|
);
|
|
58
55
|
}
|
|
59
56
|
}
|