trickle-backend 0.1.65 → 0.1.66

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/index.js CHANGED
@@ -9,4 +9,35 @@ const cloud_migrations_1 = require("./db/cloud-migrations");
9
9
  const PORT = parseInt(process.env.PORT || "4888", 10);
10
10
  server_1.app.listen(PORT, () => {
11
11
  console.log(`[trickle] Backend listening on http://localhost:${PORT}`);
12
+ if (process.env.NODE_ENV === "production") {
13
+ console.log(`[trickle] Production mode enabled`);
14
+ }
12
15
  });
16
+ // ── Data retention — periodic cleanup of expired data ──
17
+ const RETENTION_DAYS = parseInt(process.env.TRICKLE_RETENTION_DAYS || "30", 10);
18
+ const CLEANUP_INTERVAL_MS = 6 * 3600_000; // Every 6 hours
19
+ function runDataRetention() {
20
+ try {
21
+ // Delete expired share links
22
+ const expiredLinks = connection_1.db.prepare("DELETE FROM share_links WHERE expires_at IS NOT NULL AND expires_at < datetime('now')").run();
23
+ // Delete old push history (keep last 30 days)
24
+ const oldHistory = connection_1.db.prepare(`DELETE FROM push_history WHERE pushed_at < datetime('now', '-${RETENTION_DAYS} days')`).run();
25
+ // Delete stale project data (not updated in retention period)
26
+ const staleData = connection_1.db.prepare(`DELETE FROM project_data WHERE pushed_at < datetime('now', '-${RETENTION_DAYS} days')`).run();
27
+ const total = (expiredLinks.changes || 0) + (oldHistory.changes || 0) + (staleData.changes || 0);
28
+ if (total > 0) {
29
+ console.log(`[trickle] Data retention: cleaned ${total} rows (${RETENTION_DAYS}d retention)`);
30
+ // Reclaim space
31
+ try {
32
+ connection_1.db.pragma("wal_checkpoint(TRUNCATE)");
33
+ }
34
+ catch { }
35
+ }
36
+ }
37
+ catch (err) {
38
+ console.error("[trickle] Data retention error:", err.message);
39
+ }
40
+ }
41
+ // Run retention on startup and periodically
42
+ setTimeout(runDataRetention, 10_000); // 10s after startup
43
+ setInterval(runDataRetention, CLEANUP_INTERVAL_MS);
package/dist/server.js CHANGED
@@ -21,8 +21,56 @@ const search_1 = __importDefault(require("./routes/search"));
21
21
  const cloud_1 = __importDefault(require("./routes/cloud"));
22
22
  const app = (0, express_1.default)();
23
23
  exports.app = app;
24
- app.use((0, cors_1.default)());
25
- app.use(express_1.default.json({ limit: "5mb" }));
24
+ // ── Production middleware ──
25
+ // CORS allow all origins for local dev, restrict in production
26
+ const allowedOrigins = process.env.TRICKLE_CORS_ORIGINS?.split(",") || [];
27
+ app.use((0, cors_1.default)(allowedOrigins.length > 0 ? { origin: allowedOrigins } : {}));
28
+ // Body size limits
29
+ app.use(express_1.default.json({ limit: "10mb" }));
30
+ // Rate limiting — simple in-memory token bucket per IP
31
+ const rateLimits = new Map();
32
+ const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
33
+ const RATE_LIMIT_MAX = parseInt(process.env.TRICKLE_RATE_LIMIT || "300", 10); // 300 req/min default
34
+ function rateLimit(req, res, next) {
35
+ if (process.env.NODE_ENV !== "production" && !process.env.TRICKLE_RATE_LIMIT) {
36
+ return next(); // Skip rate limiting in dev unless explicitly enabled
37
+ }
38
+ const ip = req.ip || req.socket.remoteAddress || "unknown";
39
+ const now = Date.now();
40
+ let bucket = rateLimits.get(ip);
41
+ if (!bucket || now > bucket.resetAt) {
42
+ bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
43
+ rateLimits.set(ip, bucket);
44
+ }
45
+ bucket.count++;
46
+ if (bucket.count > RATE_LIMIT_MAX) {
47
+ res.status(429).json({ error: "Rate limit exceeded. Try again later." });
48
+ return;
49
+ }
50
+ // Periodic cleanup of old entries
51
+ if (rateLimits.size > 10000) {
52
+ for (const [key, val] of rateLimits) {
53
+ if (now > val.resetAt)
54
+ rateLimits.delete(key);
55
+ }
56
+ }
57
+ next();
58
+ }
59
+ app.use("/api/v1", rateLimit);
60
+ // Request logging in production
61
+ if (process.env.NODE_ENV === "production") {
62
+ app.use((req, _res, next) => {
63
+ const start = Date.now();
64
+ _res.on("finish", () => {
65
+ const ms = Date.now() - start;
66
+ if (ms > 1000 || _res.statusCode >= 400) {
67
+ console.log(`${req.method} ${req.path} ${_res.statusCode} ${ms}ms`);
68
+ }
69
+ });
70
+ next();
71
+ });
72
+ }
73
+ // ── Routes ──
26
74
  app.use("/api/ingest", ingest_1.default);
27
75
  app.use("/api/functions", functions_1.default);
28
76
  app.use("/api/types", types_1.default);
@@ -38,5 +86,10 @@ app.use("/api/search", search_1.default);
38
86
  app.use("/api/v1", cloud_1.default);
39
87
  // Health check
40
88
  app.get("/api/health", (_req, res) => {
41
- res.json({ ok: true, timestamp: new Date().toISOString() });
89
+ res.json({ ok: true, timestamp: new Date().toISOString(), version: process.env.npm_package_version || "dev" });
90
+ });
91
+ // Global error handler
92
+ app.use((err, _req, res, _next) => {
93
+ console.error("[trickle] Unhandled error:", err.message);
94
+ res.status(500).json({ error: "Internal server error" });
42
95
  });
package/fly.toml ADDED
@@ -0,0 +1,31 @@
1
+ app = "trickle-cloud"
2
+ primary_region = "lhr"
3
+
4
+ [build]
5
+ dockerfile = "Dockerfile"
6
+
7
+ [env]
8
+ PORT = "4888"
9
+ TRICKLE_DB_PATH = "/data/trickle.db"
10
+ NODE_ENV = "production"
11
+
12
+ [http_service]
13
+ internal_port = 4888
14
+ force_https = true
15
+ auto_stop_machines = "stop"
16
+ auto_start_machines = true
17
+ min_machines_running = 0
18
+
19
+ [http_service.concurrency]
20
+ type = "connections"
21
+ hard_limit = 100
22
+ soft_limit = 80
23
+
24
+ [mounts]
25
+ source = "trickle_data"
26
+ destination = "/data"
27
+
28
+ [[vm]]
29
+ memory = "512mb"
30
+ cpu_kind = "shared"
31
+ cpus = 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-backend",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "tsc",
package/src/index.ts CHANGED
@@ -10,4 +10,44 @@ const PORT = parseInt(process.env.PORT || "4888", 10);
10
10
 
11
11
  app.listen(PORT, () => {
12
12
  console.log(`[trickle] Backend listening on http://localhost:${PORT}`);
13
+ if (process.env.NODE_ENV === "production") {
14
+ console.log(`[trickle] Production mode enabled`);
15
+ }
13
16
  });
17
+
18
+ // ── Data retention — periodic cleanup of expired data ──
19
+
20
+ const RETENTION_DAYS = parseInt(process.env.TRICKLE_RETENTION_DAYS || "30", 10);
21
+ const CLEANUP_INTERVAL_MS = 6 * 3600_000; // Every 6 hours
22
+
23
+ function runDataRetention(): void {
24
+ try {
25
+ // Delete expired share links
26
+ const expiredLinks = db.prepare(
27
+ "DELETE FROM share_links WHERE expires_at IS NOT NULL AND expires_at < datetime('now')"
28
+ ).run();
29
+
30
+ // Delete old push history (keep last 30 days)
31
+ const oldHistory = db.prepare(
32
+ `DELETE FROM push_history WHERE pushed_at < datetime('now', '-${RETENTION_DAYS} days')`
33
+ ).run();
34
+
35
+ // Delete stale project data (not updated in retention period)
36
+ const staleData = db.prepare(
37
+ `DELETE FROM project_data WHERE pushed_at < datetime('now', '-${RETENTION_DAYS} days')`
38
+ ).run();
39
+
40
+ const total = (expiredLinks.changes || 0) + (oldHistory.changes || 0) + (staleData.changes || 0);
41
+ if (total > 0) {
42
+ console.log(`[trickle] Data retention: cleaned ${total} rows (${RETENTION_DAYS}d retention)`);
43
+ // Reclaim space
44
+ try { db.pragma("wal_checkpoint(TRUNCATE)"); } catch {}
45
+ }
46
+ } catch (err: any) {
47
+ console.error("[trickle] Data retention error:", err.message);
48
+ }
49
+ }
50
+
51
+ // Run retention on startup and periodically
52
+ setTimeout(runDataRetention, 10_000); // 10s after startup
53
+ setInterval(runDataRetention, CLEANUP_INTERVAL_MS);
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- import express from "express";
1
+ import express, { Request, Response, NextFunction } from "express";
2
2
  import cors from "cors";
3
3
 
4
4
  import ingestRouter from "./routes/ingest";
@@ -17,8 +17,67 @@ import cloudRouter from "./routes/cloud";
17
17
 
18
18
  const app = express();
19
19
 
20
- app.use(cors());
21
- app.use(express.json({ limit: "5mb" }));
20
+ // ── Production middleware ──
21
+
22
+ // CORS — allow all origins for local dev, restrict in production
23
+ const allowedOrigins = process.env.TRICKLE_CORS_ORIGINS?.split(",") || [];
24
+ app.use(cors(allowedOrigins.length > 0 ? { origin: allowedOrigins } : {}));
25
+
26
+ // Body size limits
27
+ app.use(express.json({ limit: "10mb" }));
28
+
29
+ // Rate limiting — simple in-memory token bucket per IP
30
+ const rateLimits = new Map<string, { count: number; resetAt: number }>();
31
+ const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
32
+ const RATE_LIMIT_MAX = parseInt(process.env.TRICKLE_RATE_LIMIT || "300", 10); // 300 req/min default
33
+
34
+ function rateLimit(req: Request, res: Response, next: NextFunction): void {
35
+ if (process.env.NODE_ENV !== "production" && !process.env.TRICKLE_RATE_LIMIT) {
36
+ return next(); // Skip rate limiting in dev unless explicitly enabled
37
+ }
38
+
39
+ const ip = req.ip || req.socket.remoteAddress || "unknown";
40
+ const now = Date.now();
41
+ let bucket = rateLimits.get(ip);
42
+
43
+ if (!bucket || now > bucket.resetAt) {
44
+ bucket = { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
45
+ rateLimits.set(ip, bucket);
46
+ }
47
+
48
+ bucket.count++;
49
+ if (bucket.count > RATE_LIMIT_MAX) {
50
+ res.status(429).json({ error: "Rate limit exceeded. Try again later." });
51
+ return;
52
+ }
53
+
54
+ // Periodic cleanup of old entries
55
+ if (rateLimits.size > 10000) {
56
+ for (const [key, val] of rateLimits) {
57
+ if (now > val.resetAt) rateLimits.delete(key);
58
+ }
59
+ }
60
+
61
+ next();
62
+ }
63
+
64
+ app.use("/api/v1", rateLimit);
65
+
66
+ // Request logging in production
67
+ if (process.env.NODE_ENV === "production") {
68
+ app.use((req: Request, _res: Response, next: NextFunction) => {
69
+ const start = Date.now();
70
+ _res.on("finish", () => {
71
+ const ms = Date.now() - start;
72
+ if (ms > 1000 || _res.statusCode >= 400) {
73
+ console.log(`${req.method} ${req.path} ${_res.statusCode} ${ms}ms`);
74
+ }
75
+ });
76
+ next();
77
+ });
78
+ }
79
+
80
+ // ── Routes ──
22
81
 
23
82
  app.use("/api/ingest", ingestRouter);
24
83
  app.use("/api/functions", functionsRouter);
@@ -36,7 +95,13 @@ app.use("/api/v1", cloudRouter);
36
95
 
37
96
  // Health check
38
97
  app.get("/api/health", (_req, res) => {
39
- res.json({ ok: true, timestamp: new Date().toISOString() });
98
+ res.json({ ok: true, timestamp: new Date().toISOString(), version: process.env.npm_package_version || "dev" });
99
+ });
100
+
101
+ // Global error handler
102
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
103
+ console.error("[trickle] Unhandled error:", err.message);
104
+ res.status(500).json({ error: "Internal server error" });
40
105
  });
41
106
 
42
107
  export { app };