lean-claudient-daemon 0.1.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/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @lean-claudient/daemon
2
+
3
+ **Background daemon service** — Phase 5B for continuous monitoring and self-healing.
4
+
5
+ ## Features
6
+
7
+ - Long-running background service
8
+ - Token consumption monitoring
9
+ - Self-healing automation
10
+ - Persistent state management
11
+ - Service lifecycle management
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @lean-claudient/daemon
17
+ ```
18
+
19
+ ## CLI
20
+
21
+ ```bash
22
+ lean-claudient-daemon start
23
+ lean-claudient-daemon stop
24
+ lean-claudient-daemon status
25
+ ```
26
+
27
+ ## Development
28
+
29
+ ```bash
30
+ npm run build # Compile TypeScript
31
+ npm run dev # Watch mode
32
+ npm run test # Run tests
33
+ npm run lint # ESLint
34
+ npm run type-check # TypeScript validation
35
+ ```
36
+
37
+ ## License
38
+
39
+ MIT
package/dist/api.js ADDED
@@ -0,0 +1,54 @@
1
+ import { Router } from 'express';
2
+ export function createApiRouter(store) {
3
+ const router = Router();
4
+ router.get('/status', (req, res) => {
5
+ const uptime = process.uptime();
6
+ const memory = process.memoryUsage().heapUsed / 1024 / 1024;
7
+ res.json({
8
+ running: true,
9
+ uptime: Math.floor(uptime),
10
+ memory: parseFloat(memory.toFixed(2)),
11
+ pid: process.pid,
12
+ });
13
+ });
14
+ router.get('/metrics', async (req, res) => {
15
+ try {
16
+ const metrics = await store.getAggregateMetrics();
17
+ res.json(metrics);
18
+ }
19
+ catch (error) {
20
+ res.status(500).json({ error: 'Failed to retrieve metrics' });
21
+ }
22
+ });
23
+ router.get('/budget/:sessionId', async (req, res) => {
24
+ try {
25
+ const budget = await store.getBudgetStatus(req.params.sessionId);
26
+ if (!budget) {
27
+ return res.status(404).json({ error: 'Session not found' });
28
+ }
29
+ res.json(budget);
30
+ }
31
+ catch (error) {
32
+ res.status(500).json({ error: 'Failed to retrieve budget' });
33
+ }
34
+ });
35
+ router.get('/sessions', async (req, res) => {
36
+ try {
37
+ const sessions = await store.getAllSessions();
38
+ res.json(sessions);
39
+ }
40
+ catch (error) {
41
+ res.status(500).json({ error: 'Failed to retrieve sessions' });
42
+ }
43
+ });
44
+ router.post('/sessions/:id/checkpoint', async (req, res) => {
45
+ try {
46
+ await store.saveCheckpoint(req.params.id, req.body.checkpoint);
47
+ res.json({ saved: true });
48
+ }
49
+ catch (error) {
50
+ res.status(500).json({ error: 'Failed to save checkpoint' });
51
+ }
52
+ });
53
+ return router;
54
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { LeanClaudientDaemon } from '../service.js';
4
+ const program = new Command();
5
+ program
6
+ .name('lean-claudient-daemon')
7
+ .version('0.1.0')
8
+ .option('-p, --port <port>', 'Port to listen on', '9831')
9
+ .option('-l, --log <path>', 'Log file path')
10
+ .action(async (options) => {
11
+ const daemon = new LeanClaudientDaemon(parseInt(options.port), options.log);
12
+ await daemon.start();
13
+ });
14
+ program.parse();
@@ -0,0 +1,31 @@
1
+ export function aggregateMetrics(records) {
2
+ if (records.length === 0) {
3
+ return {
4
+ totalInputTokens: 0,
5
+ totalOutputTokens: 0,
6
+ totalTokens: 0,
7
+ totalCost: '$0.00',
8
+ totalSavedTokens: 0,
9
+ averageCompressionRatio: 0,
10
+ sessionCount: 0,
11
+ averageSubagentCount: 0,
12
+ };
13
+ }
14
+ const totalInputTokens = records.reduce((sum, r) => sum + r.inputTokens, 0);
15
+ const totalOutputTokens = records.reduce((sum, r) => sum + r.outputTokens, 0);
16
+ const totalTokens = totalInputTokens + totalOutputTokens;
17
+ const totalCost = totalInputTokens * 0.000003 + totalOutputTokens * 0.000015;
18
+ const totalSavedTokens = records.reduce((sum, r) => sum + r.savedTokens, 0);
19
+ const avgCompressionRatio = records.reduce((sum, r) => sum + r.compressionRatio, 0) / records.length;
20
+ const avgSubagentCount = records.reduce((sum, r) => sum + r.subagentCount, 0) / records.length;
21
+ return {
22
+ totalInputTokens,
23
+ totalOutputTokens,
24
+ totalTokens,
25
+ totalCost: `$${totalCost.toFixed(2)}`,
26
+ totalSavedTokens,
27
+ averageCompressionRatio: parseFloat(avgCompressionRatio.toFixed(2)),
28
+ sessionCount: records.length,
29
+ averageSubagentCount: Math.round(avgSubagentCount),
30
+ };
31
+ }
@@ -0,0 +1,84 @@
1
+ import sqlite3 from 'sqlite3';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { aggregateMetrics } from './metrics.js';
6
+ export class MetricsStore {
7
+ constructor(dbPath) {
8
+ this.dbPath = dbPath || path.join(process.env.HOME, '.lean-claudient', 'daemon.db');
9
+ }
10
+ async initialize() {
11
+ const dir = path.dirname(this.dbPath);
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ this.db = new sqlite3.Database(this.dbPath);
16
+ const run = promisify(this.db.run.bind(this.db));
17
+ await run(`
18
+ CREATE TABLE IF NOT EXISTS metrics (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ sessionId TEXT NOT NULL,
21
+ timestamp INTEGER NOT NULL,
22
+ inputTokens INTEGER,
23
+ outputTokens INTEGER,
24
+ cost REAL,
25
+ savedTokens INTEGER,
26
+ compressionRatio REAL,
27
+ subagentCount INTEGER,
28
+ UNIQUE(sessionId, timestamp)
29
+ )
30
+ `);
31
+ await run(`
32
+ CREATE TABLE IF NOT EXISTS checkpoints (
33
+ sessionId TEXT PRIMARY KEY,
34
+ checkpoint TEXT NOT NULL,
35
+ timestamp INTEGER NOT NULL
36
+ )
37
+ `);
38
+ }
39
+ async recordMetrics(metrics) {
40
+ const run = promisify(this.db.run.bind(this.db));
41
+ await run(`INSERT OR REPLACE INTO metrics (sessionId, timestamp, inputTokens, outputTokens, cost, savedTokens, compressionRatio, subagentCount)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
43
+ metrics.sessionId,
44
+ metrics.timestamp,
45
+ metrics.inputTokens,
46
+ metrics.outputTokens,
47
+ metrics.cost,
48
+ metrics.savedTokens,
49
+ metrics.compressionRatio,
50
+ metrics.subagentCount,
51
+ ]);
52
+ }
53
+ async getAggregateMetrics() {
54
+ const all = promisify(this.db.all.bind(this.db));
55
+ const records = (await all('SELECT * FROM metrics WHERE timestamp > ? ORDER BY timestamp DESC LIMIT 1000', [Date.now() - 7 * 24 * 60 * 60 * 1000]));
56
+ return aggregateMetrics(records);
57
+ }
58
+ async getBudgetStatus(sessionId) {
59
+ const get = promisify(this.db.get.bind(this.db));
60
+ const record = (await get('SELECT * FROM metrics WHERE sessionId = ? ORDER BY timestamp DESC LIMIT 1', [sessionId]));
61
+ if (!record)
62
+ return null;
63
+ return {
64
+ sessionId,
65
+ spent: record.cost,
66
+ limit: 500,
67
+ remaining: 500 - record.cost,
68
+ alert: record.cost > 375 ? 'warning' : 'ok',
69
+ };
70
+ }
71
+ async getAllSessions() {
72
+ const all = promisify(this.db.all.bind(this.db));
73
+ return (await all('SELECT DISTINCT sessionId, MIN(timestamp) as startTime, COUNT(*) as recordCount FROM metrics GROUP BY sessionId'));
74
+ }
75
+ async saveCheckpoint(sessionId, checkpoint) {
76
+ const run = promisify(this.db.run.bind(this.db));
77
+ await run('INSERT OR REPLACE INTO checkpoints (sessionId, checkpoint, timestamp) VALUES (?, ?, ?)', [sessionId, checkpoint, Date.now()]);
78
+ }
79
+ async close() {
80
+ return new Promise((resolve, reject) => {
81
+ this.db.close((err) => (err ? reject(err) : resolve()));
82
+ });
83
+ }
84
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Rate Limiting Middleware
3
+ * Prevents DOS attacks by limiting requests per IP and session
4
+ */
5
+ /**
6
+ * Rate limit store - stores request counts by key
7
+ * Key format: "ip:address" or "session:sessionId"
8
+ */
9
+ class RateLimitStore {
10
+ constructor() {
11
+ this.store = new Map();
12
+ // Clean up expired entries every minute
13
+ this.cleanupInterval = setInterval(() => {
14
+ const now = Date.now();
15
+ for (const [key, value] of this.store.entries()) {
16
+ if (value.resetTime < now) {
17
+ this.store.delete(key);
18
+ }
19
+ }
20
+ }, 60000);
21
+ }
22
+ /**
23
+ * Record a request and return remaining quota
24
+ */
25
+ recordRequest(key, windowMs, limit) {
26
+ const now = Date.now();
27
+ const entry = this.store.get(key);
28
+ if (!entry || entry.resetTime < now) {
29
+ this.store.set(key, { count: 1, resetTime: now + windowMs });
30
+ return limit - 1;
31
+ }
32
+ entry.count++;
33
+ return Math.max(0, limit - entry.count);
34
+ }
35
+ /**
36
+ * Check if limit is exceeded
37
+ */
38
+ isLimited(key, windowMs, limit) {
39
+ const now = Date.now();
40
+ const entry = this.store.get(key);
41
+ if (!entry || entry.resetTime < now) {
42
+ return false;
43
+ }
44
+ return entry.count >= limit;
45
+ }
46
+ /**
47
+ * Get remaining time until reset
48
+ */
49
+ getResetTime(key) {
50
+ const entry = this.store.get(key);
51
+ if (!entry)
52
+ return 0;
53
+ return Math.max(0, entry.resetTime - Date.now());
54
+ }
55
+ /**
56
+ * Cleanup
57
+ */
58
+ destroy() {
59
+ clearInterval(this.cleanupInterval);
60
+ this.store.clear();
61
+ }
62
+ }
63
+ const globalStore = new RateLimitStore();
64
+ /**
65
+ * Create rate limit middleware
66
+ */
67
+ export function rateLimit(config) {
68
+ const { windowMs = 15 * 60 * 1000, // 15 minutes default
69
+ max = 100, keyGenerator = (req) => `ip:${req.ip}`, skip, onLimitReached, } = config;
70
+ return (req, res, next) => {
71
+ if (skip && skip(req)) {
72
+ return next();
73
+ }
74
+ const key = keyGenerator(req);
75
+ const remaining = globalStore.recordRequest(key, windowMs, max);
76
+ const isLimited = globalStore.isLimited(key, windowMs, max);
77
+ const resetTime = globalStore.getResetTime(key);
78
+ // Set rate limit headers
79
+ res.setHeader('X-RateLimit-Limit', max);
80
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining));
81
+ res.setHeader('X-RateLimit-Reset', Math.ceil((Date.now() + resetTime) / 1000));
82
+ if (isLimited) {
83
+ res.setHeader('Retry-After', Math.ceil(resetTime / 1000));
84
+ if (onLimitReached) {
85
+ onLimitReached(req, key);
86
+ }
87
+ return res.status(429).json({
88
+ error: 'Too Many Requests',
89
+ code: 'RATE_LIMIT_EXCEEDED',
90
+ details: {
91
+ retryAfter: Math.ceil(resetTime / 1000),
92
+ limit: max,
93
+ windowMs,
94
+ },
95
+ });
96
+ }
97
+ next();
98
+ };
99
+ }
100
+ /**
101
+ * Generic rate limiter by key
102
+ */
103
+ export function createRateLimiter(config) {
104
+ return rateLimit(config);
105
+ }
106
+ /**
107
+ * Rate limiter by IP address
108
+ */
109
+ export function createIPRateLimiter(maxRequests, windowMinutes = 15) {
110
+ return rateLimit({
111
+ windowMs: windowMinutes * 60 * 1000,
112
+ max: maxRequests,
113
+ keyGenerator: (req) => `ip:${req.ip || 'unknown'}`,
114
+ });
115
+ }
116
+ /**
117
+ * Rate limiter by session ID
118
+ */
119
+ export function createSessionRateLimiter(maxRequests, windowMinutes = 15) {
120
+ return rateLimit({
121
+ windowMs: windowMinutes * 60 * 1000,
122
+ max: maxRequests,
123
+ keyGenerator: (req) => {
124
+ const sessionId = req.params.sessionId || req.query.sessionId;
125
+ return `session:${sessionId || 'unknown'}`;
126
+ },
127
+ });
128
+ }
129
+ /**
130
+ * Rate limiter by user ID (from query or body)
131
+ */
132
+ export function createUserRateLimiter(maxRequests, windowMinutes = 15) {
133
+ return rateLimit({
134
+ windowMs: windowMinutes * 60 * 1000,
135
+ max: maxRequests,
136
+ keyGenerator: (req) => {
137
+ const userId = req.query.userId || req.body?.userId;
138
+ return `user:${userId || 'anonymous'}`;
139
+ },
140
+ });
141
+ }
142
+ /**
143
+ * Tiered rate limiter configuration
144
+ * Different limits for different endpoints
145
+ */
146
+ export const RATE_LIMITS = {
147
+ // High frequency - status checks
148
+ STATUS: { max: 100, windowMinutes: 1 },
149
+ // Medium frequency - metrics reads
150
+ METRICS: { max: 60, windowMinutes: 1 },
151
+ // Medium frequency - budget checks
152
+ BUDGET: { max: 60, windowMinutes: 1 },
153
+ // Low frequency - session creation
154
+ SESSIONS: { max: 30, windowMinutes: 1 },
155
+ // Very low frequency - checkpoints
156
+ CHECKPOINT: { max: 10, windowMinutes: 1 },
157
+ // Low frequency - admin operations
158
+ ADMIN: { max: 20, windowMinutes: 1 },
159
+ };
160
+ /**
161
+ * Create logging function for rate limit violations
162
+ */
163
+ export function createRateLimitLogger(logFile) {
164
+ return (req, key) => {
165
+ const logEntry = {
166
+ timestamp: new Date().toISOString(),
167
+ key,
168
+ method: req.method,
169
+ path: req.path,
170
+ ip: req.ip,
171
+ userAgent: req.get('User-Agent'),
172
+ };
173
+ if (logFile) {
174
+ // Would log to file in production
175
+ console.warn('[RATE_LIMIT]', JSON.stringify(logEntry));
176
+ }
177
+ else {
178
+ console.warn('[RATE_LIMIT]', logEntry);
179
+ }
180
+ };
181
+ }
182
+ /**
183
+ * Cleanup rate limiter
184
+ */
185
+ export function destroyRateLimiter() {
186
+ globalStore.destroy();
187
+ }
188
+ /**
189
+ * Reset rate limiter for testing
190
+ */
191
+ export function resetRateLimiter() {
192
+ destroyRateLimiter();
193
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Security Headers Middleware
3
+ * Adds security-related HTTP headers to all responses
4
+ */
5
+ /**
6
+ * Default security headers configuration
7
+ */
8
+ const DEFAULT_CONFIG = {
9
+ hstsMaxAge: 31536000, // 1 year
10
+ hstsIncludeSubdomains: true,
11
+ hstsPreload: true,
12
+ noSniff: true,
13
+ frameOptions: 'DENY',
14
+ xssProtection: true,
15
+ referrerPolicy: 'strict-origin-when-cross-origin',
16
+ cspDirectives: {
17
+ 'default-src': "'none'",
18
+ 'script-src': "'self'",
19
+ 'style-src': "'self'",
20
+ 'img-src': "'self'",
21
+ 'font-src': "'self'",
22
+ 'connect-src': "'self'",
23
+ 'frame-ancestors': "'none'",
24
+ 'base-uri': "'self'",
25
+ 'form-action': "'self'",
26
+ },
27
+ };
28
+ /**
29
+ * Create security headers middleware
30
+ */
31
+ export function securityHeaders(config = {}) {
32
+ const finalConfig = { ...DEFAULT_CONFIG, ...config };
33
+ return (req, res, next) => {
34
+ // Strict-Transport-Security (HSTS)
35
+ if (finalConfig.hstsMaxAge !== undefined) {
36
+ let hstsValue = `max-age=${finalConfig.hstsMaxAge}`;
37
+ if (finalConfig.hstsIncludeSubdomains) {
38
+ hstsValue += '; includeSubDomains';
39
+ }
40
+ if (finalConfig.hstsPreload) {
41
+ hstsValue += '; preload';
42
+ }
43
+ res.setHeader('Strict-Transport-Security', hstsValue);
44
+ }
45
+ // X-Content-Type-Options
46
+ if (finalConfig.noSniff !== false) {
47
+ res.setHeader('X-Content-Type-Options', 'nosniff');
48
+ }
49
+ // X-Frame-Options
50
+ if (finalConfig.frameOptions) {
51
+ res.setHeader('X-Frame-Options', finalConfig.frameOptions);
52
+ }
53
+ // X-XSS-Protection
54
+ if (finalConfig.xssProtection !== false) {
55
+ res.setHeader('X-XSS-Protection', '1; mode=block');
56
+ }
57
+ // Referrer-Policy
58
+ if (finalConfig.referrerPolicy) {
59
+ res.setHeader('Referrer-Policy', finalConfig.referrerPolicy);
60
+ }
61
+ // Content-Security-Policy (CSP)
62
+ if (finalConfig.cspDirectives) {
63
+ const cspValue = Object.entries(finalConfig.cspDirectives)
64
+ .map(([key, value]) => `${key} ${value}`)
65
+ .join('; ');
66
+ res.setHeader('Content-Security-Policy', cspValue);
67
+ }
68
+ // Permissions-Policy
69
+ if (finalConfig.permissionsPolicy) {
70
+ const ppValue = Object.entries(finalConfig.permissionsPolicy)
71
+ .map(([key, value]) => `${key}=(${value})`)
72
+ .join(', ');
73
+ res.setHeader('Permissions-Policy', ppValue);
74
+ }
75
+ // Additional security headers
76
+ res.setHeader('X-Powered-By', 'Lean Claudient');
77
+ res.setHeader('Server', 'Lean Claudient Daemon');
78
+ next();
79
+ };
80
+ }
81
+ /**
82
+ * Middleware for API endpoints (stricter CSP)
83
+ */
84
+ export function securityHeadersAPI(config = {}) {
85
+ const apiConfig = {
86
+ ...DEFAULT_CONFIG,
87
+ cspDirectives: {
88
+ 'default-src': "'none'",
89
+ 'script-src': "'none'",
90
+ 'style-src': "'none'",
91
+ 'img-src': "'none'",
92
+ 'font-src': "'none'",
93
+ 'connect-src': "'none'",
94
+ 'frame-ancestors': "'none'",
95
+ 'base-uri': "'self'",
96
+ 'form-action': "'none'",
97
+ },
98
+ ...config,
99
+ };
100
+ return securityHeaders(apiConfig);
101
+ }
102
+ /**
103
+ * Remove potentially dangerous headers
104
+ */
105
+ export function removeDangerousHeaders(req, res, next) {
106
+ // Remove headers that might leak information
107
+ const dangerous = ['X-Powered-By', 'Server', 'X-AspNet-Version', 'X-Runtime-Version'];
108
+ for (const header of dangerous) {
109
+ res.removeHeader(header);
110
+ }
111
+ // Disable caching for sensitive data
112
+ if (req.path.includes('/api/')) {
113
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
114
+ res.setHeader('Pragma', 'no-cache');
115
+ res.setHeader('Expires', '0');
116
+ }
117
+ next();
118
+ }
119
+ /**
120
+ * Validate request headers
121
+ */
122
+ export function validateRequestHeaders(req, res, next) {
123
+ const dangerousPatterns = [
124
+ /javascript:/i,
125
+ /on\w+=/i, // onclick, onload, etc
126
+ /eval\(/i,
127
+ ];
128
+ // Check headers for suspicious content
129
+ for (const [key, value] of Object.entries(req.headers)) {
130
+ if (typeof value !== 'string')
131
+ continue;
132
+ for (const pattern of dangerousPatterns) {
133
+ if (pattern.test(value)) {
134
+ return res.status(400).json({
135
+ error: 'Invalid request header',
136
+ code: 'INVALID_HEADER',
137
+ details: {
138
+ header: key,
139
+ message: 'Header contains suspicious content',
140
+ },
141
+ });
142
+ }
143
+ }
144
+ }
145
+ next();
146
+ }
147
+ /**
148
+ * Enforce HTTPS redirect for non-local environments
149
+ */
150
+ export function enforceHTTPS(req, res, next) {
151
+ // Skip for localhost and internal IPs
152
+ if (req.hostname === 'localhost' ||
153
+ req.hostname === '127.0.0.1' ||
154
+ req.ip === '::1' ||
155
+ req.ip?.startsWith('127.') ||
156
+ req.ip?.startsWith('192.168.') ||
157
+ req.ip?.startsWith('10.')) {
158
+ return next();
159
+ }
160
+ // Check if connection is secure
161
+ const isSecure = req.secure || req.get('X-Forwarded-Proto') === 'https';
162
+ if (!isSecure) {
163
+ return res.redirect(301, `https://${req.get('Host')}${req.url}`);
164
+ }
165
+ next();
166
+ }
167
+ /**
168
+ * Add Content Security Policy meta tag (for responses with HTML)
169
+ */
170
+ export function cspMetaTag(config = {}) {
171
+ const finalConfig = { ...DEFAULT_CONFIG, ...config };
172
+ return (req, res, next) => {
173
+ // Store CSP config on response for template use
174
+ if (finalConfig.cspDirectives) {
175
+ const cspValue = Object.entries(finalConfig.cspDirectives)
176
+ .map(([key, value]) => `${key} ${value}`)
177
+ .join('; ');
178
+ res.locals = { cspMetaTag: cspValue };
179
+ }
180
+ next();
181
+ };
182
+ }
183
+ /**
184
+ * Prevent information disclosure
185
+ */
186
+ export function preventInformationDisclosure(req, res, next) {
187
+ const originalJson = res.json;
188
+ res.json = function (body) {
189
+ // Remove stack traces from error responses in production
190
+ if (process.env.NODE_ENV === 'production') {
191
+ if (body && typeof body === 'object') {
192
+ delete body.stack;
193
+ delete body.stackTrace;
194
+ delete body.internal;
195
+ }
196
+ }
197
+ return originalJson.call(this, body);
198
+ };
199
+ next();
200
+ }
201
+ /**
202
+ * Create all security headers middleware stack
203
+ */
204
+ export function createSecurityStack(config = {}) {
205
+ return [
206
+ validateRequestHeaders,
207
+ removeDangerousHeaders,
208
+ preventInformationDisclosure,
209
+ securityHeaders(config),
210
+ ];
211
+ }
212
+ /**
213
+ * Strict security headers for admin endpoints
214
+ */
215
+ export const strictSecurityHeaders = securityHeaders({
216
+ hstsMaxAge: 31536000,
217
+ frameOptions: 'DENY',
218
+ cspDirectives: {
219
+ 'default-src': "'none'",
220
+ 'script-src': "'self'",
221
+ 'style-src': "'self'",
222
+ 'img-src': "'self'",
223
+ 'font-src': "'self'",
224
+ 'connect-src': "'self'",
225
+ 'frame-ancestors': "'none'",
226
+ 'base-uri': "'self'",
227
+ 'form-action': "'self'",
228
+ },
229
+ });
230
+ /**
231
+ * Relaxed security headers for public APIs (JSON only)
232
+ */
233
+ export const apiSecurityHeaders = securityHeaders({
234
+ cspDirectives: {
235
+ 'default-src': "'none'",
236
+ 'frame-ancestors': "'none'",
237
+ 'base-uri': "'self'",
238
+ },
239
+ });