-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 1: Security fixes, error handling, and test infrastructure #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "permissions": { | ||
| "allow": [ | ||
| "Bash(npx tsc *)", | ||
| "Bash(pnpm test:all)", | ||
| "Bash(pnpm install *)", | ||
| "Bash(pnpm test:all -- --reporter=verbose)", | ||
| "Bash(npm run *)", | ||
| "Bash(cd /d C:\\\\Users\\\\SSTECH\\\\developments\\\\code-kit-ultra\\\\.claude\\\\worktrees\\\\vibrant-lederberg-080dba)", | ||
| "Bash(npm test *)", | ||
| "Bash(pnpm test *)", | ||
| "Bash(pnpm run *)" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| const envSchema = z.object({ | ||
| DATABASE_URL: z.string() | ||
| .url('DATABASE_URL must be a valid URL') | ||
| .startsWith('postgresql://', 'DATABASE_URL must start with postgresql://'), | ||
| REDIS_URL: z.string() | ||
| .url('REDIS_URL must be a valid URL') | ||
| .startsWith('redis://', 'REDIS_URL must start with redis://'), | ||
| JWT_SECRET: z.string() | ||
| .min(32, 'JWT_SECRET must be at least 32 characters'), | ||
| NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'), | ||
| PORT: z.coerce.number().default(7474), | ||
| CKU_ALLOWED_ORIGINS: z.string().default('http://localhost:7473'), | ||
| LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), | ||
| INSFORGE_API_KEY: z.string().optional(), | ||
| INSFORGE_PROJECT_ID: z.string().optional(), | ||
| INSFORGE_API_BASE_URL: z.string().url().optional(), | ||
| }); | ||
|
|
||
| export type EnvConfig = z.infer<typeof envSchema>; | ||
|
|
||
| let configCache: EnvConfig | null = null; | ||
|
|
||
| export function validateEnv(): EnvConfig { | ||
| try { | ||
| return envSchema.parse(process.env); | ||
| } catch (error) { | ||
| if (error instanceof z.ZodError) { | ||
| const messages = error.errors | ||
| .map((err) => `${err.path.join('.')}: ${err.message}`) | ||
| .join('\n'); | ||
| console.error('Environment validation failed:\n' + messages); | ||
| process.exit(1); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| export function getConfig(): EnvConfig { | ||
| if (!configCache) { | ||
| // Skip validation in test mode - tests set required env vars via vitest.setup.ts | ||
| if (process.env.NODE_ENV === 'test') { | ||
| // Return a minimal config for tests | ||
| configCache = { | ||
|
Check failure on line 45 in apps/control-service/src/config/env.ts
|
||
| DATABASE_URL: process.env.DATABASE_URL || 'postgresql://test:test@localhost:5432/cku_test', | ||
| REDIS_URL: process.env.REDIS_URL || 'redis://localhost:6379/1', | ||
| JWT_SECRET: process.env.JWT_SECRET || 'test-secret-key-at-least-32-characters-long-enough', | ||
| NODE_ENV: 'test', | ||
| PORT: 7474, | ||
| CKU_ALLOWED_ORIGINS: 'http://localhost:3000,http://localhost:7473', | ||
| LOG_LEVEL: 'info', | ||
| } as EnvConfig; | ||
| } else { | ||
| configCache = validateEnv(); | ||
| } | ||
| } | ||
| return configCache; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,19 @@ import { loadRunBundle } from "../../../../packages/memory/src/run-store.js"; | |
|
|
||
| export async function listRunsHandler(req: Request, res: Response) { | ||
| try { | ||
| const auth = (req as any).auth; | ||
| const auth = req.auth; | ||
| if (!auth) { | ||
| return res.status(401).json({ error: "Unauthorized" }); | ||
| } | ||
| if (!auth.actor?.id || !auth.actor?.actorId || !auth.tenant?.orgId) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with πΒ / π. |
||
| return res.status(401).json({ error: "Unauthorized: Incomplete auth context" }); | ||
| } | ||
|
|
||
| const runs = RunReader.getRuns(); | ||
|
|
||
| // Filter to only what matches the user's org | ||
| const filtered = runs.filter((r: any) => { | ||
| const filtered = runs.filter((r) => { | ||
| if (!r?.id) return false; | ||
| const state = loadRunBundle(r.id)?.state; | ||
| if (!state) return true; // Legacy fallback | ||
| if (state.orgId && state.orgId !== auth.tenant.orgId) return false; | ||
|
|
@@ -18,7 +26,7 @@ export async function listRunsHandler(req: Request, res: Response) { | |
|
|
||
| writeAuditEvent({ | ||
| action: "LIST_RUNS", | ||
| actorName: auth.actor.actorName, | ||
| actorName: auth.actor.actorName || auth.actor.actorId, | ||
| actorId: auth.actor.actorId, | ||
| actorType: auth.actor.actorType, | ||
| orgId: auth.tenant.orgId, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This switches session revocation to
auth.actor.actorId, but the revocation API is keyed by JWTjti(revokeSession(jti, ...)/isRevoked(jti)). Using actor ID means multiple active tokens for the same actor collide on one revocation key, so deleting one session can revoke all of that actorβs concurrent sessions instead of only the current token.Useful? React with πΒ / π.