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