apteva 0.4.57 → 0.7.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/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +15 -76
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. package/src/web/types.ts +0 -298
package/src/auth/index.ts DELETED
@@ -1,394 +0,0 @@
1
- import { createHmac, randomBytes } from "crypto";
2
- import { join } from "path";
3
- import { homedir } from "os";
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
- import { UserDB, SessionDB, generateId, type User } from "../db";
6
-
7
- // ============ Configuration ============
8
-
9
- const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes in seconds
10
- const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days in seconds
11
-
12
- // ============ Secret Key Management ============
13
-
14
- let authSecret: string | null = null;
15
-
16
- function getAuthSecretPath(): string {
17
- const homeDir = process.env.DATA_DIR || join(homedir(), ".apteva");
18
- return join(homeDir, "auth.key");
19
- }
20
-
21
- function getAuthSecret(): string {
22
- if (authSecret) return authSecret;
23
-
24
- if (process.env.AUTH_SECRET) {
25
- authSecret = process.env.AUTH_SECRET;
26
- return authSecret;
27
- }
28
-
29
- const secretPath = getAuthSecretPath();
30
- if (existsSync(secretPath)) {
31
- authSecret = readFileSync(secretPath, "utf-8").trim();
32
- return authSecret;
33
- }
34
-
35
- authSecret = randomBytes(64).toString("hex");
36
-
37
- const dir = process.env.DATA_DIR || join(homedir(), ".apteva");
38
- if (!existsSync(dir)) {
39
- mkdirSync(dir, { recursive: true });
40
- }
41
- writeFileSync(secretPath, authSecret, { mode: 0o600 });
42
-
43
- return authSecret;
44
- }
45
-
46
- // ============ Password Hashing ============
47
-
48
- export async function hashPassword(password: string): Promise<string> {
49
- return await Bun.password.hash(password, {
50
- algorithm: "bcrypt",
51
- cost: 12,
52
- });
53
- }
54
-
55
- export async function verifyPassword(password: string, hash: string): Promise<boolean> {
56
- try {
57
- return await Bun.password.verify(password, hash);
58
- } catch {
59
- return false;
60
- }
61
- }
62
-
63
- // ============ Password Validation ============
64
-
65
- export interface PasswordValidation {
66
- valid: boolean;
67
- errors: string[];
68
- }
69
-
70
- export function validatePassword(password: string): PasswordValidation {
71
- const errors: string[] = [];
72
-
73
- if (password.length < 8) {
74
- errors.push("Password must be at least 8 characters");
75
- }
76
- if (!/[a-z]/.test(password)) {
77
- errors.push("Password must contain a lowercase letter");
78
- }
79
- if (!/[A-Z]/.test(password)) {
80
- errors.push("Password must contain an uppercase letter");
81
- }
82
- if (!/[0-9]/.test(password)) {
83
- errors.push("Password must contain a number");
84
- }
85
-
86
- return { valid: errors.length === 0, errors };
87
- }
88
-
89
- // ============ JWT Token Handling ============
90
-
91
- interface TokenPayload {
92
- userId: string;
93
- username: string;
94
- role: string;
95
- type: "access" | "refresh";
96
- iat: number;
97
- exp: number;
98
- }
99
-
100
- function base64UrlEncode(data: string): string {
101
- return Buffer.from(data)
102
- .toString("base64")
103
- .replace(/\+/g, "-")
104
- .replace(/\//g, "_")
105
- .replace(/=/g, "");
106
- }
107
-
108
- function base64UrlDecode(data: string): string {
109
- const padded = data + "=".repeat((4 - (data.length % 4)) % 4);
110
- return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString();
111
- }
112
-
113
- function createToken(payload: Omit<TokenPayload, "iat" | "exp">, expiresIn: number): string {
114
- const secret = getAuthSecret();
115
- const now = Math.floor(Date.now() / 1000);
116
-
117
- const fullPayload: TokenPayload = {
118
- ...payload,
119
- iat: now,
120
- exp: now + expiresIn,
121
- };
122
-
123
- const header = { alg: "HS256", typ: "JWT" };
124
- const headerEncoded = base64UrlEncode(JSON.stringify(header));
125
- const payloadEncoded = base64UrlEncode(JSON.stringify(fullPayload));
126
-
127
- const signature = createHmac("sha256", secret)
128
- .update(`${headerEncoded}.${payloadEncoded}`)
129
- .digest("base64")
130
- .replace(/\+/g, "-")
131
- .replace(/\//g, "_")
132
- .replace(/=/g, "");
133
-
134
- return `${headerEncoded}.${payloadEncoded}.${signature}`;
135
- }
136
-
137
- function verifyToken(token: string): TokenPayload | null {
138
- try {
139
- const secret = getAuthSecret();
140
- const [headerEncoded, payloadEncoded, signature] = token.split(".");
141
-
142
- if (!headerEncoded || !payloadEncoded || !signature) {
143
- return null;
144
- }
145
-
146
- const expectedSignature = createHmac("sha256", secret)
147
- .update(`${headerEncoded}.${payloadEncoded}`)
148
- .digest("base64")
149
- .replace(/\+/g, "-")
150
- .replace(/\//g, "_")
151
- .replace(/=/g, "");
152
-
153
- if (signature !== expectedSignature) {
154
- return null;
155
- }
156
-
157
- const payload = JSON.parse(base64UrlDecode(payloadEncoded)) as TokenPayload;
158
- const now = Math.floor(Date.now() / 1000);
159
-
160
- if (payload.exp < now) {
161
- return null;
162
- }
163
-
164
- return payload;
165
- } catch {
166
- return null;
167
- }
168
- }
169
-
170
- // ============ Token Generation ============
171
-
172
- export function generateAccessToken(user: User): string {
173
- return createToken(
174
- { userId: user.id, username: user.username, role: user.role, type: "access" },
175
- ACCESS_TOKEN_EXPIRY
176
- );
177
- }
178
-
179
- export function generateRefreshToken(user: User): string {
180
- return createToken(
181
- { userId: user.id, username: user.username, role: user.role, type: "refresh" },
182
- REFRESH_TOKEN_EXPIRY
183
- );
184
- }
185
-
186
- export function verifyAccessToken(token: string): TokenPayload | null {
187
- const payload = verifyToken(token);
188
- if (!payload || payload.type !== "access") {
189
- return null;
190
- }
191
- return payload;
192
- }
193
-
194
- export function verifyRefreshToken(token: string): TokenPayload | null {
195
- const payload = verifyToken(token);
196
- if (!payload || payload.type !== "refresh") {
197
- return null;
198
- }
199
- return payload;
200
- }
201
-
202
- // ============ Session Management ============
203
-
204
- export function hashToken(token: string): string {
205
- return createHmac("sha256", getAuthSecret()).update(token).digest("hex");
206
- }
207
-
208
- export interface AuthTokens {
209
- accessToken: string;
210
- refreshToken: string;
211
- expiresIn: number;
212
- }
213
-
214
- export async function createSession(user: User): Promise<AuthTokens> {
215
- const accessToken = generateAccessToken(user);
216
- const refreshToken = generateRefreshToken(user);
217
- const refreshTokenHash = hashToken(refreshToken);
218
-
219
- const expiresAt = new Date();
220
- expiresAt.setSeconds(expiresAt.getSeconds() + REFRESH_TOKEN_EXPIRY);
221
-
222
- SessionDB.create({
223
- user_id: user.id,
224
- refresh_token_hash: refreshTokenHash,
225
- expires_at: expiresAt.toISOString(),
226
- });
227
-
228
- UserDB.updateLastLogin(user.id);
229
-
230
- return { accessToken, refreshToken, expiresIn: ACCESS_TOKEN_EXPIRY };
231
- }
232
-
233
- export async function refreshSession(refreshToken: string): Promise<AuthTokens | null> {
234
- const payload = verifyRefreshToken(refreshToken);
235
- if (!payload) {
236
- return null;
237
- }
238
-
239
- const tokenHash = hashToken(refreshToken);
240
- const session = SessionDB.findByTokenHash(tokenHash);
241
- if (!session) {
242
- return null;
243
- }
244
-
245
- if (new Date(session.expires_at) < new Date()) {
246
- SessionDB.delete(session.id);
247
- return null;
248
- }
249
-
250
- const user = UserDB.findById(payload.userId);
251
- if (!user) {
252
- SessionDB.delete(session.id);
253
- return null;
254
- }
255
-
256
- SessionDB.delete(session.id);
257
- return createSession(user);
258
- }
259
-
260
- export function invalidateSession(refreshToken: string): boolean {
261
- const tokenHash = hashToken(refreshToken);
262
- return SessionDB.deleteByTokenHash(tokenHash);
263
- }
264
-
265
- export function invalidateAllSessions(userId: string): number {
266
- return SessionDB.deleteByUser(userId);
267
- }
268
-
269
- // ============ Auth Check ============
270
-
271
- export interface AuthStatus {
272
- hasUsers: boolean;
273
- authenticated: boolean;
274
- isDev: boolean;
275
- user?: { id: string; username: string; role: string };
276
- }
277
-
278
- // Determine if we're in dev mode - used by frontend for dev-only features
279
- function checkIsDev(): boolean {
280
- return process.env.NODE_ENV !== "production";
281
- }
282
-
283
- export function getAuthStatus(accessToken?: string): AuthStatus {
284
- const hasUsers = UserDB.hasUsers();
285
- const isDev = checkIsDev();
286
-
287
- if (!accessToken) {
288
- return { hasUsers, authenticated: false, isDev };
289
- }
290
-
291
- const payload = verifyAccessToken(accessToken);
292
- if (!payload) {
293
- return { hasUsers, authenticated: false, isDev };
294
- }
295
-
296
- const user = UserDB.findById(payload.userId);
297
- if (!user) {
298
- return { hasUsers, authenticated: false, isDev };
299
- }
300
-
301
- return {
302
- hasUsers,
303
- authenticated: true,
304
- isDev,
305
- user: { id: user.id, username: user.username, role: user.role },
306
- };
307
- }
308
-
309
- // ============ User Creation ============
310
-
311
- export interface CreateUserResult {
312
- success: boolean;
313
- user?: User;
314
- error?: string;
315
- }
316
-
317
- export async function createUser(data: {
318
- username: string;
319
- password: string;
320
- email?: string;
321
- role?: "admin" | "user";
322
- }): Promise<CreateUserResult> {
323
- const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
324
- if (!usernameRegex.test(data.username)) {
325
- return { success: false, error: "Username must be 3-20 characters (letters, numbers, underscore)" };
326
- }
327
-
328
- const existing = UserDB.findByUsername(data.username);
329
- if (existing) {
330
- return { success: false, error: "Username already taken" };
331
- }
332
-
333
- if (data.email) {
334
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
335
- if (!emailRegex.test(data.email)) {
336
- return { success: false, error: "Invalid email format" };
337
- }
338
- }
339
-
340
- const passwordValidation = validatePassword(data.password);
341
- if (!passwordValidation.valid) {
342
- return { success: false, error: passwordValidation.errors.join(". ") };
343
- }
344
-
345
- const passwordHash = await hashPassword(data.password);
346
-
347
- const user = UserDB.create({
348
- username: data.username,
349
- password_hash: passwordHash,
350
- email: data.email || null,
351
- role: data.role || "user",
352
- });
353
-
354
- return { success: true, user };
355
- }
356
-
357
- // ============ Login ============
358
-
359
- export interface LoginResult {
360
- success: boolean;
361
- tokens?: AuthTokens;
362
- user?: { id: string; username: string; role: string };
363
- error?: string;
364
- }
365
-
366
- export async function login(username: string, password: string): Promise<LoginResult> {
367
- const user = UserDB.findByUsername(username);
368
- if (!user) {
369
- return { success: false, error: "Invalid username or password" };
370
- }
371
-
372
- const valid = await verifyPassword(password, user.password_hash);
373
- if (!valid) {
374
- return { success: false, error: "Invalid username or password" };
375
- }
376
-
377
- const tokens = await createSession(user);
378
-
379
- return {
380
- success: true,
381
- tokens,
382
- user: { id: user.id, username: user.username, role: user.role },
383
- };
384
- }
385
-
386
- // ============ Cleanup ============
387
-
388
- export function cleanupExpiredSessions(): number {
389
- return SessionDB.deleteExpired();
390
- }
391
-
392
- // ============ Exports ============
393
-
394
- export { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY };
@@ -1,213 +0,0 @@
1
- import { verifyAccessToken, getAuthStatus } from "./index";
2
- import { UserDB, ApiKeyDB, type User } from "../db";
3
-
4
- // Extend Request type to include user
5
- declare global {
6
- interface Request {
7
- user?: User;
8
- }
9
- }
10
-
11
- // Helper to create JSON response
12
- function json(data: unknown, status = 200): Response {
13
- return new Response(JSON.stringify(data), {
14
- status,
15
- headers: { "Content-Type": "application/json" },
16
- });
17
- }
18
-
19
- // Public paths that don't require authentication
20
- const PUBLIC_PATHS = [
21
- "/api/auth/check",
22
- "/api/auth/login",
23
- "/api/auth/refresh",
24
- "/api/health",
25
- "/api/features", // Feature flags needed before auth
26
- "/api/telemetry", // Agents POST telemetry here
27
- "/api/telemetry/stream", // SSE doesn't support auth headers
28
- "/api/mcp/platform", // Built-in MCP server for agent communication
29
- "/api/webhooks/composio", // Composio trigger webhooks (HMAC-verified)
30
- "/api/webhooks/agentdojo", // AgentDojo trigger webhooks (HMAC-verified)
31
- ];
32
-
33
- // Regex patterns for public paths (for dynamic segments)
34
- const PUBLIC_PATTERNS = [
35
- /^\/api\/mcp\/servers\/[^/]+\/mcp$/, // Local MCP server JSON-RPC endpoints
36
- ];
37
-
38
- // Path prefixes that don't require authentication (for agent communication)
39
- const PUBLIC_PREFIXES = [
40
- "/api/agents/", // Agent chat API needs to work without frontend auth
41
- ];
42
-
43
- // Paths that only work when no users exist
44
- const SETUP_PATHS = [
45
- "/api/onboarding/user",
46
- ];
47
-
48
- // Admin-only paths
49
- const ADMIN_PATHS = [
50
- "/api/users",
51
- ];
52
-
53
- export interface AuthContext {
54
- user: User | null;
55
- isAuthenticated: boolean;
56
- isAdmin: boolean;
57
- }
58
-
59
- /**
60
- * Auth middleware for protecting API routes
61
- * Returns null if request should proceed, or a Response to return immediately
62
- */
63
- export async function authMiddleware(req: Request, path: string): Promise<{ response?: Response; context: AuthContext }> {
64
- const context: AuthContext = {
65
- user: null,
66
- isAuthenticated: false,
67
- isAdmin: false,
68
- };
69
-
70
- // Check if auth is disabled
71
- if (process.env.AUTH_ENABLED === "false") {
72
- return { context };
73
- }
74
-
75
- // Public paths - no auth needed
76
- if (PUBLIC_PATHS.some(p => path === p || path.startsWith(p + "/"))) {
77
- return { context };
78
- }
79
-
80
- // Public patterns - no auth needed (dynamic path segments)
81
- if (PUBLIC_PATTERNS.some(p => p.test(path))) {
82
- return { context };
83
- }
84
-
85
- // Public prefixes - no auth needed (for agent communication)
86
- if (PUBLIC_PREFIXES.some(p => path.startsWith(p))) {
87
- return { context };
88
- }
89
-
90
- // Setup paths - only allowed when no users exist
91
- if (SETUP_PATHS.some(p => path === p || path.startsWith(p + "/"))) {
92
- const hasUsers = UserDB.hasUsers();
93
- if (!hasUsers) {
94
- return { context }; // Allow - no users yet
95
- }
96
- // Users exist - require auth for these paths
97
- }
98
-
99
- // Check for API key authentication (X-API-Key header or Bearer apt_... token)
100
- const apiKeyHeader = req.headers.get("X-API-Key");
101
- const authHeader = req.headers.get("Authorization");
102
- const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
103
-
104
- const apiKeyValue = apiKeyHeader || (bearerToken?.startsWith("apt_") ? bearerToken : null);
105
-
106
- if (apiKeyValue) {
107
- // API key auth
108
- const result = ApiKeyDB.validate(apiKeyValue);
109
- if (!result) {
110
- return {
111
- response: json({ error: "Invalid or expired API key", code: "INVALID_API_KEY" }, 401),
112
- context,
113
- };
114
- }
115
-
116
- context.user = result.user;
117
- context.isAuthenticated = true;
118
- context.isAdmin = result.user.role === "admin";
119
- } else if (bearerToken) {
120
- // JWT auth
121
- const payload = verifyAccessToken(bearerToken);
122
-
123
- if (!payload) {
124
- return {
125
- response: json({ error: "Invalid or expired token", code: "INVALID_TOKEN" }, 401),
126
- context,
127
- };
128
- }
129
-
130
- const user = UserDB.findById(payload.userId);
131
- if (!user) {
132
- return {
133
- response: json({ error: "User not found", code: "USER_NOT_FOUND" }, 401),
134
- context,
135
- };
136
- }
137
-
138
- context.user = user;
139
- context.isAuthenticated = true;
140
- context.isAdmin = user.role === "admin";
141
- } else {
142
- return {
143
- response: json({ error: "Unauthorized", code: "NO_TOKEN" }, 401),
144
- context,
145
- };
146
- }
147
-
148
- // Check admin-only paths
149
- if (ADMIN_PATHS.some(p => path === p || path.startsWith(p + "/"))) {
150
- if (!context.isAdmin) {
151
- return {
152
- response: json({ error: "Admin access required", code: "FORBIDDEN" }, 403),
153
- context,
154
- };
155
- }
156
- }
157
-
158
- return { context };
159
- }
160
-
161
- /**
162
- * Helper to check if a path requires authentication
163
- */
164
- export function requiresAuth(path: string): boolean {
165
- if (process.env.AUTH_ENABLED === "false") {
166
- return false;
167
- }
168
-
169
- if (PUBLIC_PATHS.some(p => path === p || path.startsWith(p + "/"))) {
170
- return false;
171
- }
172
-
173
- return true;
174
- }
175
-
176
- /**
177
- * Helper to extract token from request
178
- */
179
- export function getTokenFromRequest(req: Request): string | null {
180
- const authHeader = req.headers.get("Authorization");
181
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
182
- return null;
183
- }
184
- return authHeader.slice(7);
185
- }
186
-
187
- /**
188
- * Helper to extract refresh token from cookie
189
- */
190
- export function getRefreshTokenFromCookie(req: Request): string | null {
191
- const cookie = req.headers.get("Cookie");
192
- if (!cookie) return null;
193
-
194
- const match = cookie.match(/(?:^|;\s*)apteva_session=([^;]+)/);
195
- return match ? match[1] : null;
196
- }
197
-
198
- /**
199
- * Create Set-Cookie header for refresh token
200
- * Note: Secure flag only added when HTTPS is explicitly enabled (not just production mode)
201
- * This allows Docker deployments to work on localhost without HTTPS
202
- */
203
- export function createRefreshTokenCookie(token: string, maxAge: number): string {
204
- const secure = process.env.HTTPS_ENABLED === "true" ? "; Secure" : "";
205
- return `apteva_session=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${maxAge}${secure}`;
206
- }
207
-
208
- /**
209
- * Create Set-Cookie header to clear refresh token
210
- */
211
- export function clearRefreshTokenCookie(): string {
212
- return "apteva_session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0";
213
- }