Version: 2.0 Last Updated: 2026-01-18 Status: Pre-production security guidance
gitleaks detect).env files in .gitignorepnpm audit)Authentication is implemented via JWT bearer tokens with role-based access control.
sub, email, roles, permissions, profileIdjose for JWT operationscreateAuthMiddleware() — Required auth (401 if missing/invalid)createOptionalAuthMiddleware() — Proceeds without user context if no tokencreateOwnershipMiddleware() — Ensures user owns the resource (compares profileId)createAdminMiddleware() — Requires admin role (403 if not admin)A three-tier onRequest hook applies authentication globally:
/health, /ready, /metrics, /plans/public-profiles, /taxonomy/profiles, /masks, /epochs, /stagesSupport social login via:
For Web3 users:
GraphQL WebSocket subscriptions (GET /graphql/ws) require JWT authentication at connection time:
onConnect validates JWT from connectionParams.authorization headerfalse)jwtAuth is not configured (dev mode without JWT secret)| Role | Permissions | |——|————-| | Owner | Full access to own profile, billing, data export | | Reviewer | Read-only access to public profile views | | Admin | System administration (internal only) | | Agent | API access with scoped permissions |
// Check ownership before modification
function canModifyProfile(user: User, profile: Profile): boolean {
return profile.identityId === user.identityId || user.role === 'admin';
}
// Planned: apps/api/src/middleware/authorize.ts
export function requireAuth(req, reply, done) {
if (!req.session?.userId) {
return reply.code(401).send({ error: 'Unauthorized' });
}
done();
}
export function requireOwner(req, reply, done) {
if (req.params.profileId !== req.session.profileId) {
return reply.code(403).send({ error: 'Forbidden' });
}
done();
}
For Hunter Protocol and external integrations:
GET /healthGET /readyGET /taxonomy/* (reference data)All profile CRUD, narrative generation, export endpoints.
GET /metrics - Prometheus metricsPOST /webhooks/github - GitHub webhook ingestionSecurity headers are enforced via @fastify/helmet in apps/api/src/index.ts:
fastify.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'wss:', 'https:'],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
});
Headers set automatically on every response:
nosniff)SAMEORIGIN)Note: Helmet is disabled in test mode to avoid CSP interfering with test harnesses.
app.register(require('@fastify/cors'), {
origin: [
'https://your-domain.com',
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
].filter(Boolean),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
});
app.register(require('@fastify/multipart'), {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB max file
files: 5, // Max 5 files per request
}
});
| Environment | Database | Credentials |
|————-|———-|————-|
| Development | midst_dev | Local dev creds |
| Testing | midst_test | Isolated test creds |
| Integration | midst_integration | CI-specific creds |
| Production | midst_prod | Vault-managed creds |
Always use parameterized queries:
// CORRECT - Parameterized query
const result = await pool.query(
'SELECT * FROM profiles WHERE id = $1',
[profileId]
);
// WRONG - String concatenation (SQL injection risk)
// Never do this: `SELECT * FROM profiles WHERE id = '${profileId}'`
-- App user should NOT have superuser privileges
CREATE USER app_user WITH PASSWORD '...';
GRANT CONNECT ON DATABASE midst_prod TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
-- Deny DROP, TRUNCATE, CREATE
| Environment | Method | Tool |
|---|---|---|
| Local Dev | .env.local |
File (gitignored) |
| CI/CD | Environment variables | GitHub Secrets |
| Production | Secret manager | 1Password / HashiCorp Vault |
# Load environment from 1Password
# ~/.config/op/load-env.sh
# Project-specific loader
# /path/to/project/*.env.op.sh
op inject -i .env.template -o .env
| Secret | Used By | Rotation |
|——–|———|———-|
| DATABASE_URL | API, Orchestrator | On compromise |
| REDIS_URL | API, Orchestrator | On compromise |
| GITHUB_WEBHOOK_SECRET | Orchestrator | Monthly |
| SERPER_API_KEY | Hunter Protocol | On compromise |
| STRIPE_SECRET_KEY | API (billing) | Annual |
| SESSION_SECRET | API (auth) | Quarterly |
# Pre-commit hook to detect secrets
pnpm add -D gitleaks
gitleaks detect --source . --verbose
# CI integration
# - name: Secret Scan
# run: gitleaks detect --source . --fail
// apps/api/src/validation/profile.ts
import { z } from 'zod';
export const CreateProfileSchema = z.object({
displayName: z.string().min(1).max(100).trim(),
slug: z.string().regex(/^[a-z0-9-]+$/).max(50),
title: z.string().max(200).optional(),
headline: z.string().max(500).optional(),
summaryMarkdown: z.string().max(10000).optional(),
});
// Use in route handler
const parsed = CreateProfileSchema.safeParse(req.body);
if (!parsed.success) {
return reply.code(400).send({ errors: parsed.error.errors });
}
const allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
];
function validateUpload(file: MultipartFile): boolean {
return (
allowedMimeTypes.includes(file.mimetype) &&
file.size <= 10 * 1024 * 1024 // 10MB
);
}
Implement multiple layers of rate limiting:
// apps/api/src/plugins/rateLimit.ts
app.register(require('@fastify/rate-limit'), {
global: true,
max: 100, // requests
timeWindow: '1 minute',
keyGenerator: (req) => req.ip,
errorResponseBuilder: (req, context) => ({
statusCode: 429,
error: 'Too Many Requests',
message: `Rate limit exceeded. Try again in ${context.after}`,
}),
});
// Stricter limit for expensive operations
app.route({
method: 'POST',
url: '/profiles/:id/narrative',
config: {
rateLimit: {
max: 10,
timeWindow: '1 minute',
}
},
handler: narrativeHandler,
});
| Tier | Requests/Min | Narrative Gen/Min | |——|————–|——————-| | Free | 30 | 5 | | Artisan | 100 | 20 | | Dramatist | 300 | 50 |
// Track per-user usage
async function trackUsage(userId: string, action: string): Promise<void> {
const key = `usage:${userId}:${action}:${getCurrentMinute()}`;
await redis.incr(key);
await redis.expire(key, 120); // 2 minute TTL
}
| Category | Events | |———-|——–| | Authentication | Login, logout, failed attempts, password reset | | Authorization | Permission denied, role changes | | Data Access | Profile views, exports, modifications | | API | Requests, errors, rate limit hits | | System | Startup, shutdown, config changes |
interface AuditLog {
timestamp: string; // ISO 8601
level: 'info' | 'warn' | 'error';
event: string; // e.g., 'profile.view', 'auth.login'
userId?: string;
profileId?: string;
ip?: string;
userAgent?: string;
details?: Record<string, unknown>;
}
// apps/api/src/services/audit.ts
export async function audit(event: AuditEvent): Promise<void> {
const log: AuditLog = {
timestamp: new Date().toISOString(),
level: 'info',
event: event.type,
userId: event.userId,
profileId: event.profileId,
ip: event.request?.ip,
userAgent: event.request?.headers['user-agent'],
details: event.details,
};
// Write to database
await db.query(
'INSERT INTO audit_logs (data) VALUES ($1)',
[JSON.stringify(log)]
);
// Also log to stdout for aggregation
console.log(JSON.stringify(log));
}
user@***)| # | Vulnerability | Mitigation | Status |
|---|---|---|---|
| 1 | Broken Access Control | Authorization middleware, ownership checks | Implemented |
| 2 | Cryptographic Failures | TLS everywhere, secure password hashing | Ready |
| 3 | Injection | Parameterized queries, Zod validation | Implemented |
| 4 | Insecure Design | Security review, threat modeling | Planned |
| 5 | Security Misconfiguration | Security headers, defaults review | Ready |
| 6 | Vulnerable Components | pnpm audit, Dependabot |
Ready |
| 7 | Authentication Failures | Session management, MFA | Planned |
| 8 | Software/Data Integrity | CI/CD security, signed commits | Planned |
| 9 | Logging Failures | Audit logging, monitoring | Planned |
| 10 | SSRF | URL validation, allowlists | Ready |
// SQL - Always parameterize
await pool.query('SELECT * FROM profiles WHERE id = $1', [id]);
// NoSQL/Redis - Validate keys
const key = `profile:${validateUUID(id)}`;
// OS Commands - Use execFile instead of exec
// See: src/utils/execFileNoThrow.ts for safe command execution
// Validate URLs before fetching
const allowedDomains = ['linkedin.com', 'github.com', 'example.com'];
function validateExternalUrl(url: string): boolean {
const parsed = new URL(url);
return (
['http:', 'https:'].includes(parsed.protocol) &&
allowedDomains.some(d => parsed.hostname.endsWith(d)) &&
!isPrivateIP(parsed.hostname)
);
}
Development Staging Production
----------- ------- ----------
midst_dev midst_stage midst_prod
Local creds Test creds Vault creds
No auth Test auth Full auth
# Use specific version, not latest
FROM node:22-alpine
# Run as non-root user
RUN addgroup -S app && adduser -S app -G app
USER app
# Don't expose unnecessary ports
EXPOSE 3001
Security issues: security@in-midst-my-life.dev
| Level | Description | Response Time | |——-|————-|—————| | Critical | Active exploit, data breach | Immediate | | High | Exploitable vulnerability | < 24 hours | | Medium | Vulnerability requiring auth | < 1 week | | Low | Minor issue, defense in depth | < 1 month |
gitleaks rules to prevent recurrence# Dependency vulnerability scan
pnpm audit
# Secret detection
gitleaks detect --source .
# SAST (future)
# Consider: Semgrep, CodeQL
Schedule professional penetration testing before production launch:
Document Authority: This document defines security requirements for the project. All code must comply with these guidelines before production deployment.
Review Schedule: Security guidelines should be reviewed quarterly and after any security incident.