trickle-backend 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.
Files changed (65) hide show
  1. package/dist/db/connection.d.ts +3 -0
  2. package/dist/db/connection.js +16 -0
  3. package/dist/db/migrations.d.ts +2 -0
  4. package/dist/db/migrations.js +51 -0
  5. package/dist/db/queries.d.ts +70 -0
  6. package/dist/db/queries.js +186 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +10 -0
  9. package/dist/routes/audit.d.ts +2 -0
  10. package/dist/routes/audit.js +251 -0
  11. package/dist/routes/codegen.d.ts +2 -0
  12. package/dist/routes/codegen.js +224 -0
  13. package/dist/routes/coverage.d.ts +2 -0
  14. package/dist/routes/coverage.js +98 -0
  15. package/dist/routes/dashboard.d.ts +2 -0
  16. package/dist/routes/dashboard.js +433 -0
  17. package/dist/routes/diff.d.ts +2 -0
  18. package/dist/routes/diff.js +181 -0
  19. package/dist/routes/errors.d.ts +2 -0
  20. package/dist/routes/errors.js +86 -0
  21. package/dist/routes/functions.d.ts +2 -0
  22. package/dist/routes/functions.js +69 -0
  23. package/dist/routes/ingest.d.ts +2 -0
  24. package/dist/routes/ingest.js +111 -0
  25. package/dist/routes/mock.d.ts +2 -0
  26. package/dist/routes/mock.js +57 -0
  27. package/dist/routes/search.d.ts +2 -0
  28. package/dist/routes/search.js +136 -0
  29. package/dist/routes/tail.d.ts +2 -0
  30. package/dist/routes/tail.js +11 -0
  31. package/dist/routes/types.d.ts +2 -0
  32. package/dist/routes/types.js +97 -0
  33. package/dist/server.d.ts +2 -0
  34. package/dist/server.js +40 -0
  35. package/dist/services/sse-broker.d.ts +10 -0
  36. package/dist/services/sse-broker.js +39 -0
  37. package/dist/services/type-differ.d.ts +2 -0
  38. package/dist/services/type-differ.js +126 -0
  39. package/dist/services/type-generator.d.ts +319 -0
  40. package/dist/services/type-generator.js +3207 -0
  41. package/dist/types.d.ts +56 -0
  42. package/dist/types.js +2 -0
  43. package/package.json +22 -0
  44. package/src/db/connection.ts +16 -0
  45. package/src/db/migrations.ts +50 -0
  46. package/src/db/queries.ts +260 -0
  47. package/src/index.ts +11 -0
  48. package/src/routes/audit.ts +283 -0
  49. package/src/routes/codegen.ts +237 -0
  50. package/src/routes/coverage.ts +120 -0
  51. package/src/routes/dashboard.ts +435 -0
  52. package/src/routes/diff.ts +215 -0
  53. package/src/routes/errors.ts +91 -0
  54. package/src/routes/functions.ts +75 -0
  55. package/src/routes/ingest.ts +139 -0
  56. package/src/routes/mock.ts +66 -0
  57. package/src/routes/search.ts +169 -0
  58. package/src/routes/tail.ts +12 -0
  59. package/src/routes/types.ts +106 -0
  60. package/src/server.ts +40 -0
  61. package/src/services/sse-broker.ts +51 -0
  62. package/src/services/type-differ.ts +141 -0
  63. package/src/services/type-generator.ts +3853 -0
  64. package/src/types.ts +37 -0
  65. package/tsconfig.json +8 -0
@@ -0,0 +1,56 @@
1
+ export type TypeNode = {
2
+ kind: "primitive";
3
+ name: "string" | "number" | "boolean" | "null" | "undefined" | "bigint" | "symbol";
4
+ } | {
5
+ kind: "array";
6
+ element: TypeNode;
7
+ } | {
8
+ kind: "object";
9
+ properties: Record<string, TypeNode>;
10
+ } | {
11
+ kind: "union";
12
+ members: TypeNode[];
13
+ } | {
14
+ kind: "function";
15
+ params: TypeNode[];
16
+ returnType: TypeNode;
17
+ } | {
18
+ kind: "promise";
19
+ resolved: TypeNode;
20
+ } | {
21
+ kind: "map";
22
+ key: TypeNode;
23
+ value: TypeNode;
24
+ } | {
25
+ kind: "set";
26
+ element: TypeNode;
27
+ } | {
28
+ kind: "tuple";
29
+ elements: TypeNode[];
30
+ } | {
31
+ kind: "unknown";
32
+ };
33
+ export interface IngestPayload {
34
+ functionName: string;
35
+ module: string;
36
+ language: "js" | "python";
37
+ environment: string;
38
+ typeHash: string;
39
+ argsType: TypeNode;
40
+ returnType: TypeNode;
41
+ sampleInput?: unknown;
42
+ sampleOutput?: unknown;
43
+ error?: {
44
+ type: string;
45
+ message: string;
46
+ stackTrace?: string;
47
+ argsSnapshot?: unknown;
48
+ };
49
+ }
50
+ export interface TypeDiff {
51
+ kind: "added" | "removed" | "changed";
52
+ path: string;
53
+ from?: TypeNode;
54
+ to?: TypeNode;
55
+ type?: TypeNode;
56
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "trickle-backend",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "scripts": {
6
+ "build": "tsc",
7
+ "dev": "ts-node src/index.ts",
8
+ "start": "node dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "better-sqlite3": "^11.0.0",
12
+ "cors": "^2.8.5",
13
+ "express": "^4.21.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/better-sqlite3": "^7.6.0",
17
+ "@types/cors": "^2.8.0",
18
+ "@types/express": "^4.17.0",
19
+ "ts-node": "^10.9.0",
20
+ "typescript": "^5.4.0"
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import Database, { Database as DatabaseType } from "better-sqlite3";
4
+
5
+ const trickleDir = path.join(process.env.HOME || "~", ".trickle");
6
+
7
+ fs.mkdirSync(trickleDir, { recursive: true });
8
+
9
+ const dbPath = path.join(trickleDir, "trickle.db");
10
+
11
+ const db: DatabaseType = new Database(dbPath);
12
+
13
+ db.pragma("journal_mode = WAL");
14
+ db.pragma("foreign_keys = ON");
15
+
16
+ export { db };
@@ -0,0 +1,50 @@
1
+ import type Database from "better-sqlite3";
2
+
3
+ export function runMigrations(db: Database.Database): void {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS functions (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ function_name TEXT NOT NULL,
8
+ module TEXT NOT NULL,
9
+ environment TEXT NOT NULL DEFAULT 'unknown',
10
+ language TEXT NOT NULL,
11
+ first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
12
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
13
+ UNIQUE(function_name, module, language)
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS type_snapshots (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ function_id INTEGER NOT NULL REFERENCES functions(id),
19
+ type_hash TEXT NOT NULL,
20
+ args_type TEXT NOT NULL,
21
+ return_type TEXT NOT NULL,
22
+ variables_type TEXT,
23
+ sample_input TEXT,
24
+ sample_output TEXT,
25
+ env TEXT NOT NULL DEFAULT 'unknown',
26
+ observed_at TEXT NOT NULL DEFAULT (datetime('now')),
27
+ UNIQUE(function_id, type_hash, env)
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS errors (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ function_id INTEGER NOT NULL REFERENCES functions(id),
33
+ error_type TEXT NOT NULL,
34
+ error_message TEXT NOT NULL,
35
+ stack_trace TEXT,
36
+ args_type TEXT,
37
+ return_type TEXT,
38
+ variables_type TEXT,
39
+ args_snapshot TEXT,
40
+ type_hash TEXT,
41
+ env TEXT NOT NULL DEFAULT 'unknown',
42
+ occurred_at TEXT NOT NULL DEFAULT (datetime('now'))
43
+ );
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_snapshots_function_id ON type_snapshots(function_id);
46
+ CREATE INDEX IF NOT EXISTS idx_errors_function_id ON errors(function_id);
47
+ CREATE INDEX IF NOT EXISTS idx_errors_occurred_at ON errors(occurred_at);
48
+ CREATE INDEX IF NOT EXISTS idx_errors_env ON errors(env);
49
+ `);
50
+ }
@@ -0,0 +1,260 @@
1
+ import type Database from "better-sqlite3";
2
+
3
+ // --- Functions ---
4
+
5
+ export function upsertFunction(
6
+ db: Database.Database,
7
+ params: { functionName: string; module: string; environment: string; language: string }
8
+ ) {
9
+ const insertStmt = db.prepare(`
10
+ INSERT OR IGNORE INTO functions (function_name, module, environment, language)
11
+ VALUES (@functionName, @module, @environment, @language)
12
+ `);
13
+ const updateStmt = db.prepare(`
14
+ UPDATE functions
15
+ SET last_seen_at = datetime('now'), environment = @environment
16
+ WHERE function_name = @functionName AND module = @module AND language = @language
17
+ `);
18
+ const selectStmt = db.prepare(`
19
+ SELECT * FROM functions
20
+ WHERE function_name = @functionName AND module = @module AND language = @language
21
+ `);
22
+
23
+ insertStmt.run(params);
24
+ updateStmt.run(params);
25
+ return selectStmt.get(params) as Record<string, unknown>;
26
+ }
27
+
28
+ // --- Type Snapshots ---
29
+
30
+ export function findSnapshotByHash(
31
+ db: Database.Database,
32
+ functionId: number,
33
+ typeHash: string,
34
+ env: string
35
+ ) {
36
+ const stmt = db.prepare(`
37
+ SELECT * FROM type_snapshots
38
+ WHERE function_id = ? AND type_hash = ? AND env = ?
39
+ `);
40
+ return stmt.get(functionId, typeHash, env) as Record<string, unknown> | undefined;
41
+ }
42
+
43
+ export function insertSnapshot(
44
+ db: Database.Database,
45
+ params: {
46
+ functionId: number;
47
+ typeHash: string;
48
+ argsType: string;
49
+ returnType: string;
50
+ variablesType?: string;
51
+ sampleInput?: string;
52
+ sampleOutput?: string;
53
+ env: string;
54
+ }
55
+ ) {
56
+ const stmt = db.prepare(`
57
+ INSERT INTO type_snapshots (function_id, type_hash, args_type, return_type, variables_type, sample_input, sample_output, env)
58
+ VALUES (@functionId, @typeHash, @argsType, @returnType, @variablesType, @sampleInput, @sampleOutput, @env)
59
+ `);
60
+ const result = stmt.run(params);
61
+ return { id: result.lastInsertRowid, ...params };
62
+ }
63
+
64
+ // --- Errors ---
65
+
66
+ export function insertError(
67
+ db: Database.Database,
68
+ params: {
69
+ functionId: number;
70
+ errorType: string;
71
+ errorMessage: string;
72
+ stackTrace?: string;
73
+ argsType?: string;
74
+ returnType?: string;
75
+ variablesType?: string;
76
+ argsSnapshot?: string;
77
+ typeHash?: string;
78
+ env: string;
79
+ }
80
+ ) {
81
+ const stmt = db.prepare(`
82
+ INSERT INTO errors (function_id, error_type, error_message, stack_trace, args_type, return_type, variables_type, args_snapshot, type_hash, env)
83
+ VALUES (@functionId, @errorType, @errorMessage, @stackTrace, @argsType, @returnType, @variablesType, @argsSnapshot, @typeHash, @env)
84
+ `);
85
+ const result = stmt.run(params);
86
+
87
+ const selectStmt = db.prepare(`SELECT * FROM errors WHERE id = ?`);
88
+ return selectStmt.get(result.lastInsertRowid) as Record<string, unknown>;
89
+ }
90
+
91
+ // --- List / Get Functions ---
92
+
93
+ export function listFunctions(
94
+ db: Database.Database,
95
+ params: { search?: string; env?: string; language?: string; limit?: number; offset?: number }
96
+ ) {
97
+ const conditions: string[] = [];
98
+ const bindings: unknown[] = [];
99
+
100
+ if (params.search) {
101
+ conditions.push("(function_name LIKE ? OR module LIKE ?)");
102
+ bindings.push(`%${params.search}%`, `%${params.search}%`);
103
+ }
104
+ if (params.env) {
105
+ conditions.push("environment = ?");
106
+ bindings.push(params.env);
107
+ }
108
+ if (params.language) {
109
+ conditions.push("language = ?");
110
+ bindings.push(params.language);
111
+ }
112
+
113
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
114
+ const limit = params.limit ?? 50;
115
+ const offset = params.offset ?? 0;
116
+
117
+ const stmt = db.prepare(`
118
+ SELECT * FROM functions ${where}
119
+ ORDER BY last_seen_at DESC
120
+ LIMIT ? OFFSET ?
121
+ `);
122
+ bindings.push(limit, offset);
123
+
124
+ const countStmt = db.prepare(`SELECT COUNT(*) as total FROM functions ${where}`);
125
+ const countBindings = bindings.slice(0, bindings.length - 2);
126
+
127
+ const rows = stmt.all(...bindings) as Record<string, unknown>[];
128
+ const total = (countStmt.get(...countBindings) as { total: number }).total;
129
+
130
+ return { rows, total };
131
+ }
132
+
133
+ export function getFunction(db: Database.Database, id: number) {
134
+ const stmt = db.prepare(`SELECT * FROM functions WHERE id = ?`);
135
+ return stmt.get(id) as Record<string, unknown> | undefined;
136
+ }
137
+
138
+ export function getFunctionByName(db: Database.Database, functionName: string) {
139
+ const stmt = db.prepare(`SELECT * FROM functions WHERE function_name LIKE ?`);
140
+ return stmt.all(`%${functionName}%`) as Record<string, unknown>[];
141
+ }
142
+
143
+ // --- List / Get Snapshots ---
144
+
145
+ export function listSnapshots(
146
+ db: Database.Database,
147
+ params: { functionId: number; env?: string; limit?: number }
148
+ ) {
149
+ const conditions: string[] = ["function_id = ?"];
150
+ const bindings: unknown[] = [params.functionId];
151
+
152
+ if (params.env) {
153
+ conditions.push("env = ?");
154
+ bindings.push(params.env);
155
+ }
156
+
157
+ const limit = params.limit ?? 50;
158
+ const where = conditions.join(" AND ");
159
+
160
+ const stmt = db.prepare(`
161
+ SELECT * FROM type_snapshots
162
+ WHERE ${where}
163
+ ORDER BY observed_at DESC
164
+ LIMIT ?
165
+ `);
166
+ bindings.push(limit);
167
+
168
+ return stmt.all(...bindings) as Record<string, unknown>[];
169
+ }
170
+
171
+ export function getLatestSnapshot(db: Database.Database, functionId: number, env?: string) {
172
+ if (env) {
173
+ const stmt = db.prepare(`
174
+ SELECT * FROM type_snapshots
175
+ WHERE function_id = ? AND env = ?
176
+ ORDER BY observed_at DESC
177
+ LIMIT 1
178
+ `);
179
+ return stmt.get(functionId, env) as Record<string, unknown> | undefined;
180
+ }
181
+
182
+ const stmt = db.prepare(`
183
+ SELECT * FROM type_snapshots
184
+ WHERE function_id = ?
185
+ ORDER BY observed_at DESC
186
+ LIMIT 1
187
+ `);
188
+ return stmt.get(functionId) as Record<string, unknown> | undefined;
189
+ }
190
+
191
+ // --- List / Get Errors ---
192
+
193
+ export function listErrors(
194
+ db: Database.Database,
195
+ params: {
196
+ functionId?: number;
197
+ functionName?: string;
198
+ env?: string;
199
+ since?: string;
200
+ limit?: number;
201
+ offset?: number;
202
+ }
203
+ ) {
204
+ const conditions: string[] = [];
205
+ const bindings: unknown[] = [];
206
+
207
+ if (params.functionId) {
208
+ conditions.push("e.function_id = ?");
209
+ bindings.push(params.functionId);
210
+ }
211
+ if (params.functionName) {
212
+ conditions.push("f.function_name LIKE ?");
213
+ bindings.push(`%${params.functionName}%`);
214
+ }
215
+ if (params.env) {
216
+ conditions.push("e.env = ?");
217
+ bindings.push(params.env);
218
+ }
219
+ if (params.since) {
220
+ conditions.push("e.occurred_at >= ?");
221
+ bindings.push(params.since);
222
+ }
223
+
224
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
225
+ const limit = params.limit ?? 50;
226
+ const offset = params.offset ?? 0;
227
+
228
+ const stmt = db.prepare(`
229
+ SELECT e.*, f.function_name, f.module, f.language
230
+ FROM errors e
231
+ JOIN functions f ON f.id = e.function_id
232
+ ${where}
233
+ ORDER BY e.occurred_at DESC
234
+ LIMIT ? OFFSET ?
235
+ `);
236
+ bindings.push(limit, offset);
237
+
238
+ const countStmt = db.prepare(`
239
+ SELECT COUNT(*) as total
240
+ FROM errors e
241
+ JOIN functions f ON f.id = e.function_id
242
+ ${where}
243
+ `);
244
+ const countBindings = bindings.slice(0, bindings.length - 2);
245
+
246
+ const rows = stmt.all(...bindings) as Record<string, unknown>[];
247
+ const total = (countStmt.get(...countBindings) as { total: number }).total;
248
+
249
+ return { rows, total };
250
+ }
251
+
252
+ export function getError(db: Database.Database, id: number) {
253
+ const stmt = db.prepare(`
254
+ SELECT e.*, f.function_name, f.module, f.language
255
+ FROM errors e
256
+ JOIN functions f ON f.id = e.function_id
257
+ WHERE e.id = ?
258
+ `);
259
+ return stmt.get(id) as Record<string, unknown> | undefined;
260
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { app } from "./server";
2
+ import { db } from "./db/connection";
3
+ import { runMigrations } from "./db/migrations";
4
+
5
+ runMigrations(db);
6
+
7
+ const PORT = parseInt(process.env.PORT || "4888", 10);
8
+
9
+ app.listen(PORT, () => {
10
+ console.log(`[trickle] Backend listening on http://localhost:${PORT}`);
11
+ });
@@ -0,0 +1,283 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { listFunctions, getLatestSnapshot } from "../db/queries";
4
+ import { TypeNode } from "../types";
5
+
6
+ const router = Router();
7
+
8
+ interface AuditIssue {
9
+ severity: "error" | "warning" | "info";
10
+ rule: string;
11
+ message: string;
12
+ route?: string;
13
+ field?: string;
14
+ }
15
+
16
+ const SENSITIVE_FIELD_NAMES = new Set([
17
+ "password", "passwd", "pass", "secret", "token", "apikey", "api_key",
18
+ "apiKey", "accesstoken", "access_token", "accessToken", "refreshtoken",
19
+ "refresh_token", "refreshToken", "privatekey", "private_key", "privateKey",
20
+ "ssn", "creditcard", "credit_card", "creditCard", "cvv", "pin",
21
+ ]);
22
+
23
+ function tryParseJson(value: string): unknown {
24
+ try {
25
+ return JSON.parse(value);
26
+ } catch {
27
+ return value;
28
+ }
29
+ }
30
+
31
+ /** Count properties in a TypeNode recursively */
32
+ function countProperties(node: TypeNode): number {
33
+ if (node.kind === "object") {
34
+ return Object.keys(node.properties).length;
35
+ }
36
+ return 0;
37
+ }
38
+
39
+ /** Get max nesting depth of a TypeNode */
40
+ function maxDepth(node: TypeNode, current: number = 0): number {
41
+ switch (node.kind) {
42
+ case "object": {
43
+ if (Object.keys(node.properties).length === 0) return current;
44
+ let max = current;
45
+ for (const val of Object.values(node.properties)) {
46
+ max = Math.max(max, maxDepth(val, current + 1));
47
+ }
48
+ return max;
49
+ }
50
+ case "array":
51
+ return maxDepth(node.element, current + 1);
52
+ case "union":
53
+ return Math.max(...node.members.map((m) => maxDepth(m, current)), current);
54
+ case "tuple":
55
+ return Math.max(...node.elements.map((e) => maxDepth(e, current + 1)), current);
56
+ case "map":
57
+ return maxDepth(node.value, current + 1);
58
+ case "set":
59
+ return maxDepth(node.element, current + 1);
60
+ case "promise":
61
+ return maxDepth(node.resolved, current);
62
+ default:
63
+ return current;
64
+ }
65
+ }
66
+
67
+ /** Check if an object mixes camelCase and snake_case */
68
+ function detectNamingInconsistency(node: TypeNode, path: string): string | null {
69
+ if (node.kind !== "object") return null;
70
+ const keys = Object.keys(node.properties);
71
+ if (keys.length < 2) return null;
72
+
73
+ let hasCamel = false;
74
+ let hasSnake = false;
75
+ for (const key of keys) {
76
+ if (key.includes("_") && key !== key.toUpperCase()) hasSnake = true;
77
+ if (/[a-z][A-Z]/.test(key)) hasCamel = true;
78
+ }
79
+ if (hasCamel && hasSnake) {
80
+ const camelKeys = keys.filter((k) => /[a-z][A-Z]/.test(k));
81
+ const snakeKeys = keys.filter((k) => k.includes("_") && k !== k.toUpperCase());
82
+ return `Mixed naming: camelCase (${camelKeys.slice(0, 3).join(", ")}) and snake_case (${snakeKeys.slice(0, 3).join(", ")})`;
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /** Find sensitive fields in a TypeNode recursively */
88
+ function findSensitiveFields(node: TypeNode, path: string, results: string[]): void {
89
+ if (node.kind === "object") {
90
+ for (const [key, val] of Object.entries(node.properties)) {
91
+ const fullPath = path ? `${path}.${key}` : key;
92
+ if (SENSITIVE_FIELD_NAMES.has(key.toLowerCase())) {
93
+ results.push(fullPath);
94
+ }
95
+ findSensitiveFields(val, fullPath, results);
96
+ }
97
+ } else if (node.kind === "array") {
98
+ findSensitiveFields(node.element, `${path}[]`, results);
99
+ } else if (node.kind === "union") {
100
+ for (const m of node.members) {
101
+ findSensitiveFields(m, path, results);
102
+ }
103
+ }
104
+ }
105
+
106
+ /** Collect all field names with their types across routes */
107
+ function collectFieldTypes(
108
+ node: TypeNode,
109
+ prefix: string,
110
+ result: Map<string, Set<string>>,
111
+ ): void {
112
+ if (node.kind === "object") {
113
+ for (const [key, val] of Object.entries(node.properties)) {
114
+ const fullKey = prefix ? `${prefix}.${key}` : key;
115
+ if (!result.has(key)) result.set(key, new Set());
116
+ if (val.kind === "primitive") {
117
+ result.get(key)!.add(val.name);
118
+ } else {
119
+ result.get(key)!.add(val.kind);
120
+ }
121
+ collectFieldTypes(val, fullKey, result);
122
+ }
123
+ } else if (node.kind === "array") {
124
+ collectFieldTypes(node.element, `${prefix}[]`, result);
125
+ }
126
+ }
127
+
128
+ router.get("/", (req: Request, res: Response) => {
129
+ try {
130
+ const { env } = req.query;
131
+ const issues: AuditIssue[] = [];
132
+
133
+ // Collect all functions with their types
134
+ const listed = listFunctions(db, {
135
+ env: env as string | undefined,
136
+ limit: 500,
137
+ });
138
+
139
+ const functions: Array<{
140
+ name: string;
141
+ argsType: TypeNode;
142
+ returnType: TypeNode;
143
+ }> = [];
144
+
145
+ for (const fn of listed.rows) {
146
+ const functionId = fn.id as number;
147
+ const functionName = fn.function_name as string;
148
+
149
+ const snapshot = getLatestSnapshot(db, functionId, env as string | undefined);
150
+ if (!snapshot) {
151
+ // Untyped function
152
+ issues.push({
153
+ severity: "warning",
154
+ rule: "no-types",
155
+ message: `No type observations recorded`,
156
+ route: functionName,
157
+ });
158
+ continue;
159
+ }
160
+
161
+ const argsType = tryParseJson(snapshot.args_type as string) as TypeNode;
162
+ const returnType = tryParseJson(snapshot.return_type as string) as TypeNode;
163
+ if (!argsType || !returnType) continue;
164
+
165
+ functions.push({ name: functionName, argsType, returnType });
166
+ }
167
+
168
+ // Analyze each function
169
+ const globalFieldTypes = new Map<string, Set<string>>();
170
+
171
+ for (const fn of functions) {
172
+ const routeName = fn.name;
173
+
174
+ // 1. Sensitive data in responses
175
+ const sensitiveFields: string[] = [];
176
+ findSensitiveFields(fn.returnType, "", sensitiveFields);
177
+ for (const field of sensitiveFields) {
178
+ issues.push({
179
+ severity: "error",
180
+ rule: "sensitive-data",
181
+ message: `Response exposes potentially sensitive field "${field}"`,
182
+ route: routeName,
183
+ field,
184
+ });
185
+ }
186
+
187
+ // 2. Oversized responses (>15 top-level properties)
188
+ const propCount = countProperties(fn.returnType);
189
+ if (propCount > 15) {
190
+ issues.push({
191
+ severity: "warning",
192
+ rule: "oversized-response",
193
+ message: `Response has ${propCount} top-level fields — consider pagination or field selection`,
194
+ route: routeName,
195
+ });
196
+ }
197
+
198
+ // 3. Deeply nested types (>4 levels)
199
+ const depth = maxDepth(fn.returnType);
200
+ if (depth > 4) {
201
+ issues.push({
202
+ severity: "warning",
203
+ rule: "deep-nesting",
204
+ message: `Response is nested ${depth} levels deep — consider flattening`,
205
+ route: routeName,
206
+ });
207
+ }
208
+
209
+ // 4. Naming inconsistency
210
+ const namingIssue = detectNamingInconsistency(fn.returnType, "");
211
+ if (namingIssue) {
212
+ issues.push({
213
+ severity: "warning",
214
+ rule: "inconsistent-naming",
215
+ message: namingIssue,
216
+ route: routeName,
217
+ });
218
+ }
219
+
220
+ // Also check args for naming issues
221
+ const argsNaming = detectNamingInconsistency(fn.argsType, "");
222
+ if (argsNaming) {
223
+ issues.push({
224
+ severity: "warning",
225
+ rule: "inconsistent-naming",
226
+ message: `Request: ${argsNaming}`,
227
+ route: routeName,
228
+ });
229
+ }
230
+
231
+ // 5. Empty response type
232
+ if (fn.returnType.kind === "unknown" ||
233
+ (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length === 0)) {
234
+ issues.push({
235
+ severity: "info",
236
+ rule: "empty-response",
237
+ message: `Response type is empty or unknown — may need more observations`,
238
+ route: routeName,
239
+ });
240
+ }
241
+
242
+ // Collect field types for cross-route consistency check
243
+ collectFieldTypes(fn.returnType, "", globalFieldTypes);
244
+ }
245
+
246
+ // 6. Cross-route type inconsistency (same field name, different types)
247
+ for (const [fieldName, types] of globalFieldTypes) {
248
+ if (types.size > 1 && !["kind", "type", "data", "value", "result", "status"].includes(fieldName)) {
249
+ const typeList = Array.from(types).sort().join(", ");
250
+ issues.push({
251
+ severity: "warning",
252
+ rule: "type-inconsistency",
253
+ message: `Field "${fieldName}" has different types across routes: ${typeList}`,
254
+ });
255
+ }
256
+ }
257
+
258
+ // Sort: errors first, then warnings, then info
259
+ const severityOrder = { error: 0, warning: 1, info: 2 };
260
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
261
+
262
+ // Summary
263
+ const errorCount = issues.filter((i) => i.severity === "error").length;
264
+ const warningCount = issues.filter((i) => i.severity === "warning").length;
265
+ const infoCount = issues.filter((i) => i.severity === "info").length;
266
+
267
+ res.json({
268
+ summary: {
269
+ total: issues.length,
270
+ errors: errorCount,
271
+ warnings: warningCount,
272
+ info: infoCount,
273
+ routesAnalyzed: functions.length,
274
+ },
275
+ issues,
276
+ });
277
+ } catch (err) {
278
+ console.error("Audit error:", err);
279
+ res.status(500).json({ error: "Internal server error" });
280
+ }
281
+ });
282
+
283
+ export default router;