@hasna/logs 0.3.25 → 0.3.27
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 +33 -10
- package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
- package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/cli/index.js +8511 -177
- package/dist/count-bmj4r2zb.js +10 -0
- package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
- package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
- package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
- package/dist/index-931pbyn5.js +141 -0
- package/dist/index-b5c72f1p.js +7 -0
- package/dist/{index-p1vgwwsz.js → index-bnr19y0h.js} +596 -37
- package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
- package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
- package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
- package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
- package/dist/index-hq6kzaah.js +26 -0
- package/dist/index-j34f36wy.js +5672 -0
- package/dist/index-p4dbdzx4.js +1849 -0
- package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
- package/dist/index-t3x838zw.js +2583 -0
- package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
- package/dist/index.js +2982 -22
- package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
- package/dist/mcp/index.js +1473 -4286
- package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
- package/dist/server/index.js +2944 -417
- package/dist/storage.js +50 -0
- package/package.json +27 -8
- package/biome.json +0 -13
- package/bun.lock +0 -376
- package/dashboard/README.md +0 -73
- package/dashboard/bun.lock +0 -526
- package/dashboard/eslint.config.js +0 -23
- package/dashboard/index.html +0 -13
- package/dashboard/package.json +0 -32
- package/dashboard/src/App.css +0 -184
- package/dashboard/src/App.tsx +0 -49
- package/dashboard/src/api.ts +0 -33
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +0 -1
- package/dashboard/src/assets/vite.svg +0 -1
- package/dashboard/src/index.css +0 -111
- package/dashboard/src/main.tsx +0 -10
- package/dashboard/src/pages/Alerts.tsx +0 -69
- package/dashboard/src/pages/Issues.tsx +0 -50
- package/dashboard/src/pages/Perf.tsx +0 -75
- package/dashboard/src/pages/Projects.tsx +0 -67
- package/dashboard/src/pages/Summary.tsx +0 -67
- package/dashboard/src/pages/Tail.tsx +0 -65
- package/dashboard/tsconfig.app.json +0 -28
- package/dashboard/tsconfig.json +0 -7
- package/dashboard/tsconfig.node.json +0 -26
- package/dashboard/vite.config.ts +0 -14
- package/dist/count-x3n7qg3c.js +0 -9
- package/dist/index-5cj74qka.js +0 -10803
- package/dist/index-997bkzr2.js +0 -15
- package/dist/index-kezb178p.js +0 -1241
- package/dist/index-pen6t0yc.js +0 -10794
- package/sdk/package.json +0 -27
- package/sdk/src/index.ts +0 -143
- package/sdk/src/types.ts +0 -56
- package/src/cli/entrypoints.test.ts +0 -63
- package/src/cli/index.ts +0 -471
- package/src/db/index.test.ts +0 -33
- package/src/db/index.ts +0 -189
- package/src/db/migrations/001_alert_rules.ts +0 -21
- package/src/db/migrations/002_issues.ts +0 -21
- package/src/db/migrations/003_retention.ts +0 -15
- package/src/db/migrations/004_page_auth.ts +0 -13
- package/src/db/pg-migrations.ts +0 -167
- package/src/index.ts +0 -1
- package/src/lib/alerts.test.ts +0 -67
- package/src/lib/alerts.ts +0 -117
- package/src/lib/browser-script.test.ts +0 -35
- package/src/lib/browser-script.ts +0 -31
- package/src/lib/compare.test.ts +0 -52
- package/src/lib/compare.ts +0 -85
- package/src/lib/count.test.ts +0 -44
- package/src/lib/count.ts +0 -55
- package/src/lib/diagnose.test.ts +0 -55
- package/src/lib/diagnose.ts +0 -91
- package/src/lib/export.test.ts +0 -66
- package/src/lib/export.ts +0 -65
- package/src/lib/github.ts +0 -38
- package/src/lib/health.test.ts +0 -48
- package/src/lib/health.ts +0 -51
- package/src/lib/ingest.test.ts +0 -57
- package/src/lib/ingest.ts +0 -78
- package/src/lib/issues.test.ts +0 -79
- package/src/lib/issues.ts +0 -70
- package/src/lib/jobs.test.ts +0 -69
- package/src/lib/jobs.ts +0 -63
- package/src/lib/lighthouse.ts +0 -65
- package/src/lib/package-meta.test.ts +0 -43
- package/src/lib/package-meta.ts +0 -80
- package/src/lib/page-auth.test.ts +0 -54
- package/src/lib/page-auth.ts +0 -48
- package/src/lib/parse-time.test.ts +0 -37
- package/src/lib/parse-time.ts +0 -14
- package/src/lib/perf.test.ts +0 -45
- package/src/lib/perf.ts +0 -46
- package/src/lib/projects.test.ts +0 -73
- package/src/lib/projects.ts +0 -69
- package/src/lib/query.test.ts +0 -104
- package/src/lib/query.ts +0 -84
- package/src/lib/retention.test.ts +0 -42
- package/src/lib/retention.ts +0 -62
- package/src/lib/rotate.test.ts +0 -37
- package/src/lib/rotate.ts +0 -27
- package/src/lib/scanner.ts +0 -131
- package/src/lib/scheduler.ts +0 -63
- package/src/lib/session-context.ts +0 -28
- package/src/lib/summarize.test.ts +0 -38
- package/src/lib/summarize.ts +0 -23
- package/src/mcp/http.test.ts +0 -92
- package/src/mcp/http.ts +0 -135
- package/src/mcp/index.test.ts +0 -27
- package/src/mcp/index.ts +0 -444
- package/src/server/index.ts +0 -61
- package/src/server/routes/alerts.ts +0 -32
- package/src/server/routes/issues.ts +0 -43
- package/src/server/routes/jobs.ts +0 -32
- package/src/server/routes/logs.ts +0 -113
- package/src/server/routes/perf.ts +0 -23
- package/src/server/routes/projects.ts +0 -67
- package/src/server/routes/stream.ts +0 -43
- package/src/server/server.test.ts +0 -194
- package/src/types/index.ts +0 -119
- package/tsconfig.json +0 -22
- /package/dashboard/{public → dist}/favicon.svg +0 -0
- /package/dashboard/{public → dist}/icons.svg +0 -0
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getPage,
|
|
4
4
|
ingestBatch,
|
|
5
|
+
ingestLog,
|
|
5
6
|
listPages,
|
|
7
|
+
redactString,
|
|
8
|
+
redactValue,
|
|
6
9
|
saveSnapshot,
|
|
7
10
|
touchPage
|
|
8
|
-
} from "./index-
|
|
11
|
+
} from "./index-p4dbdzx4.js";
|
|
12
|
+
import {
|
|
13
|
+
getEventStoreDataDir
|
|
14
|
+
} from "./index-t3x838zw.js";
|
|
9
15
|
import {
|
|
10
16
|
createScanRun,
|
|
11
17
|
finishScanRun,
|
|
12
18
|
listJobs,
|
|
13
19
|
updateJob
|
|
14
|
-
} from "./index-
|
|
20
|
+
} from "./index-e1930v9b.js";
|
|
21
|
+
import {
|
|
22
|
+
sqlBindings
|
|
23
|
+
} from "./index-b5c72f1p.js";
|
|
15
24
|
import {
|
|
16
25
|
__commonJS,
|
|
17
26
|
__require,
|
|
@@ -894,7 +903,7 @@ var require_scheduled_task = __commonJS((exports, module) => {
|
|
|
894
903
|
|
|
895
904
|
// node_modules/node-cron/src/background-scheduled-task/index.js
|
|
896
905
|
var require_background_scheduled_task = __commonJS((exports, module) => {
|
|
897
|
-
var __dirname = "/
|
|
906
|
+
var __dirname = "/home/hasna/Workspace/hasna/opensource/open-logs/node_modules/node-cron/src/background-scheduled-task";
|
|
898
907
|
var EventEmitter = __require("events");
|
|
899
908
|
var path = __require("path");
|
|
900
909
|
var { fork } = __require("child_process");
|
|
@@ -1018,23 +1027,23 @@ var TTL_BY_LEVEL = {
|
|
|
1018
1027
|
fatal: "error_ttl_hours"
|
|
1019
1028
|
};
|
|
1020
1029
|
function runRetentionForProject(db, projectId) {
|
|
1021
|
-
const project = db.prepare("SELECT * FROM projects WHERE id = $id").get({ $id: projectId });
|
|
1030
|
+
const project = db.prepare("SELECT * FROM projects WHERE id = $id").get(sqlBindings({ $id: projectId }));
|
|
1022
1031
|
if (!project)
|
|
1023
1032
|
return { deleted: 0 };
|
|
1024
1033
|
let deleted = 0;
|
|
1025
1034
|
for (const [level, configKey] of Object.entries(TTL_BY_LEVEL)) {
|
|
1026
1035
|
const ttlHours = project[configKey];
|
|
1027
1036
|
const cutoff = new Date(Date.now() - ttlHours * 3600 * 1000).toISOString();
|
|
1028
|
-
const before = db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = $p AND level = $level AND timestamp < $cutoff").get({ $p: projectId, $level: level, $cutoff: cutoff }).c;
|
|
1037
|
+
const before = db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = $p AND level = $level AND timestamp < $cutoff").get(sqlBindings({ $p: projectId, $level: level, $cutoff: cutoff })).c;
|
|
1029
1038
|
if (before > 0) {
|
|
1030
|
-
db.prepare("DELETE FROM logs WHERE project_id = $p AND level = $level AND timestamp < $cutoff").run({ $p: projectId, $level: level, $cutoff: cutoff });
|
|
1039
|
+
db.prepare("DELETE FROM logs WHERE project_id = $p AND level = $level AND timestamp < $cutoff").run(sqlBindings({ $p: projectId, $level: level, $cutoff: cutoff }));
|
|
1031
1040
|
deleted += before;
|
|
1032
1041
|
}
|
|
1033
1042
|
}
|
|
1034
|
-
const total = db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = $p").get({ $p: projectId }).c;
|
|
1043
|
+
const total = db.prepare("SELECT COUNT(*) as c FROM logs WHERE project_id = $p").get(sqlBindings({ $p: projectId })).c;
|
|
1035
1044
|
if (total > project.max_rows) {
|
|
1036
1045
|
const toDelete = total - project.max_rows;
|
|
1037
|
-
db.prepare(
|
|
1046
|
+
db.prepare("DELETE FROM logs WHERE id IN (SELECT id FROM logs WHERE project_id = $p ORDER BY timestamp ASC LIMIT $limit)").run(sqlBindings({ $p: projectId, $limit: toDelete }));
|
|
1038
1047
|
deleted += toDelete;
|
|
1039
1048
|
}
|
|
1040
1049
|
return { deleted };
|
|
@@ -1053,44 +1062,128 @@ function setRetentionPolicy(db, projectId, config) {
|
|
|
1053
1062
|
return;
|
|
1054
1063
|
const params = Object.fromEntries(Object.entries(config).map(([k, v]) => [`$${k}`, v]));
|
|
1055
1064
|
params.$id = projectId;
|
|
1056
|
-
db.prepare(`UPDATE projects SET ${fields} WHERE id = $id`).run(params);
|
|
1065
|
+
db.prepare(`UPDATE projects SET ${fields} WHERE id = $id`).run(sqlBindings(params));
|
|
1057
1066
|
}
|
|
1058
1067
|
|
|
1059
1068
|
// src/lib/page-auth.ts
|
|
1060
|
-
import {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1069
|
+
import {
|
|
1070
|
+
createCipheriv,
|
|
1071
|
+
createDecipheriv,
|
|
1072
|
+
createHash,
|
|
1073
|
+
randomBytes
|
|
1074
|
+
} from "crypto";
|
|
1075
|
+
import {
|
|
1076
|
+
chmodSync,
|
|
1077
|
+
existsSync,
|
|
1078
|
+
mkdirSync,
|
|
1079
|
+
readFileSync,
|
|
1080
|
+
writeFileSync
|
|
1081
|
+
} from "fs";
|
|
1082
|
+
import { join } from "path";
|
|
1083
|
+
var LOCAL_PAGE_AUTH_SECRET_FILE = "page-auth.key";
|
|
1084
|
+
function encrypt(db, text) {
|
|
1085
|
+
const iv = randomBytes(12);
|
|
1086
|
+
const cipher = createCipheriv("aes-256-gcm", pageAuthSecretKey(db), iv);
|
|
1087
|
+
const encrypted = Buffer.concat([
|
|
1088
|
+
cipher.update(text, "utf8"),
|
|
1089
|
+
cipher.final()
|
|
1090
|
+
]);
|
|
1091
|
+
return [
|
|
1092
|
+
"v2",
|
|
1093
|
+
iv.toString("hex"),
|
|
1094
|
+
cipher.getAuthTag().toString("hex"),
|
|
1095
|
+
encrypted.toString("hex")
|
|
1096
|
+
].join(":");
|
|
1067
1097
|
}
|
|
1068
|
-
function decrypt(text) {
|
|
1069
|
-
const
|
|
1098
|
+
function decrypt(db, text) {
|
|
1099
|
+
const parts = text.split(":");
|
|
1100
|
+
if (parts[0] === "v2") {
|
|
1101
|
+
const [, ivHex2, tagHex, encHex2] = parts;
|
|
1102
|
+
if (!ivHex2 || !tagHex || !encHex2)
|
|
1103
|
+
throw new Error("Invalid page auth secret format");
|
|
1104
|
+
const decipher2 = createDecipheriv("aes-256-gcm", pageAuthSecretKey(db), Buffer.from(ivHex2, "hex"));
|
|
1105
|
+
decipher2.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
1106
|
+
return Buffer.concat([
|
|
1107
|
+
decipher2.update(Buffer.from(encHex2, "hex")),
|
|
1108
|
+
decipher2.final()
|
|
1109
|
+
]).toString("utf8");
|
|
1110
|
+
}
|
|
1111
|
+
const [ivHex, encHex] = parts;
|
|
1070
1112
|
if (!ivHex || !encHex)
|
|
1071
1113
|
return text;
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1114
|
+
const decipher = createDecipheriv("aes-256-cbc", legacyPageAuthSecretKey(db), Buffer.from(ivHex, "hex"));
|
|
1115
|
+
return Buffer.concat([
|
|
1116
|
+
decipher.update(Buffer.from(encHex, "hex")),
|
|
1117
|
+
decipher.final()
|
|
1118
|
+
]).toString("utf8");
|
|
1076
1119
|
}
|
|
1077
1120
|
function setPageAuth(db, pageId, type, credentials) {
|
|
1078
|
-
const encrypted = encrypt(credentials);
|
|
1121
|
+
const encrypted = encrypt(db, credentials);
|
|
1079
1122
|
return db.prepare(`
|
|
1080
1123
|
INSERT INTO page_auth (page_id, type, credentials)
|
|
1081
1124
|
VALUES ($page_id, $type, $credentials)
|
|
1082
1125
|
ON CONFLICT(page_id) DO UPDATE SET type = excluded.type, credentials = excluded.credentials
|
|
1083
1126
|
RETURNING *
|
|
1084
|
-
`).get({
|
|
1127
|
+
`).get(sqlBindings({
|
|
1128
|
+
$page_id: pageId,
|
|
1129
|
+
$type: type,
|
|
1130
|
+
$credentials: encrypted
|
|
1131
|
+
}));
|
|
1085
1132
|
}
|
|
1086
1133
|
function getPageAuth(db, pageId) {
|
|
1087
|
-
const row = db.prepare("SELECT * FROM page_auth WHERE page_id = $id").get({ $id: pageId });
|
|
1134
|
+
const row = db.prepare("SELECT * FROM page_auth WHERE page_id = $id").get(sqlBindings({ $id: pageId }));
|
|
1088
1135
|
if (!row)
|
|
1089
1136
|
return null;
|
|
1090
|
-
return { type: row.type, credentials: decrypt(row.credentials) };
|
|
1137
|
+
return { type: row.type, credentials: decrypt(db, row.credentials) };
|
|
1091
1138
|
}
|
|
1092
1139
|
function deletePageAuth(db, pageId) {
|
|
1093
|
-
db.
|
|
1140
|
+
db.prepare("DELETE FROM page_auth WHERE page_id = $id").run(sqlBindings({ $id: pageId }));
|
|
1141
|
+
}
|
|
1142
|
+
function pageAuthSecretKey(db) {
|
|
1143
|
+
const secret = pageAuthSecret(db);
|
|
1144
|
+
if (/^[a-f0-9]{64}$/i.test(secret))
|
|
1145
|
+
return Buffer.from(secret, "hex");
|
|
1146
|
+
return createHash("sha256").update(secret).digest();
|
|
1147
|
+
}
|
|
1148
|
+
function legacyPageAuthSecretKey(db) {
|
|
1149
|
+
return Buffer.from(pageAuthSecret(db).padEnd(32).slice(0, 32));
|
|
1150
|
+
}
|
|
1151
|
+
function pageAuthSecret(db) {
|
|
1152
|
+
const secret = process.env.HASNA_LOGS_SECRET_KEY?.trim() || process.env.LOGS_SECRET_KEY?.trim();
|
|
1153
|
+
if (!secret)
|
|
1154
|
+
return readOrCreateLocalPageAuthSecret(db);
|
|
1155
|
+
if (secret.length < 32) {
|
|
1156
|
+
throw new Error("Page auth secret must be at least 32 characters. Generate one with: openssl rand -hex 32");
|
|
1157
|
+
}
|
|
1158
|
+
return secret;
|
|
1159
|
+
}
|
|
1160
|
+
function readOrCreateLocalPageAuthSecret(db) {
|
|
1161
|
+
const dataDir = getEventStoreDataDir(db);
|
|
1162
|
+
const secretPath = join(dataDir, LOCAL_PAGE_AUTH_SECRET_FILE);
|
|
1163
|
+
mkdirSync(dataDir, { recursive: true });
|
|
1164
|
+
if (existsSync(secretPath))
|
|
1165
|
+
return readLocalPageAuthSecret(secretPath);
|
|
1166
|
+
const secret = randomBytes(32).toString("hex");
|
|
1167
|
+
try {
|
|
1168
|
+
writeFileSync(secretPath, `${secret}
|
|
1169
|
+
`, {
|
|
1170
|
+
encoding: "utf8",
|
|
1171
|
+
flag: "wx",
|
|
1172
|
+
mode: 384
|
|
1173
|
+
});
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (error.code !== "EEXIST")
|
|
1176
|
+
throw error;
|
|
1177
|
+
}
|
|
1178
|
+
chmodSync(secretPath, 384);
|
|
1179
|
+
return readLocalPageAuthSecret(secretPath);
|
|
1180
|
+
}
|
|
1181
|
+
function readLocalPageAuthSecret(secretPath) {
|
|
1182
|
+
const secret = readFileSync(secretPath, "utf8").trim();
|
|
1183
|
+
if (secret.length < 32) {
|
|
1184
|
+
throw new Error(`Page auth secret file ${secretPath} must contain at least 32 characters.`);
|
|
1185
|
+
}
|
|
1186
|
+
return secret;
|
|
1094
1187
|
}
|
|
1095
1188
|
|
|
1096
1189
|
// src/lib/scanner.ts
|
|
@@ -1111,12 +1204,20 @@ async function scanPage(db, projectId, pageId, urlOverride) {
|
|
|
1111
1204
|
} catch {}
|
|
1112
1205
|
} else if (auth?.type === "basic") {
|
|
1113
1206
|
const [username, password] = auth.credentials.split(":");
|
|
1114
|
-
contextOptions.httpCredentials = {
|
|
1207
|
+
contextOptions.httpCredentials = {
|
|
1208
|
+
username: username ?? "",
|
|
1209
|
+
password: password ?? ""
|
|
1210
|
+
};
|
|
1115
1211
|
}
|
|
1116
1212
|
const context = await browser.newContext(contextOptions);
|
|
1117
1213
|
if (auth?.type === "bearer") {
|
|
1118
1214
|
await context.route("**/*", (route) => {
|
|
1119
|
-
route.continue({
|
|
1215
|
+
route.continue({
|
|
1216
|
+
headers: {
|
|
1217
|
+
...route.request().headers(),
|
|
1218
|
+
Authorization: `Bearer ${auth.credentials}`
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1120
1221
|
});
|
|
1121
1222
|
}
|
|
1122
1223
|
const browserPage = await context.newPage();
|
|
@@ -1157,17 +1258,18 @@ async function scanPage(db, projectId, pageId, urlOverride) {
|
|
|
1157
1258
|
url
|
|
1158
1259
|
});
|
|
1159
1260
|
});
|
|
1160
|
-
|
|
1261
|
+
const perfScore = null;
|
|
1161
1262
|
try {
|
|
1162
1263
|
await browserPage.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
|
1163
1264
|
try {
|
|
1164
1265
|
const metrics = await browserPage.evaluate(() => {
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
1266
|
+
const perf = globalThis.performance;
|
|
1267
|
+
const nav = perf.getEntriesByType("navigation")[0];
|
|
1268
|
+
const paint = perf.getEntriesByName("first-contentful-paint")[0];
|
|
1167
1269
|
return {
|
|
1168
|
-
ttfb: nav ? nav.responseStart - nav.requestStart : null,
|
|
1270
|
+
ttfb: nav?.responseStart !== undefined && nav.requestStart !== undefined ? nav.responseStart - nav.requestStart : null,
|
|
1169
1271
|
fcp: paint?.startTime ?? null,
|
|
1170
|
-
domLoad: nav ? nav.domContentLoadedEventEnd - nav.startTime : null
|
|
1272
|
+
domLoad: nav?.domContentLoadedEventEnd !== undefined && nav.startTime !== undefined ? nav.domContentLoadedEventEnd - nav.startTime : null
|
|
1171
1273
|
};
|
|
1172
1274
|
});
|
|
1173
1275
|
if (metrics.fcp !== null || metrics.ttfb !== null) {
|
|
@@ -1212,7 +1314,7 @@ function startScheduler(db) {
|
|
|
1212
1314
|
}
|
|
1213
1315
|
function scheduleJob(db, jobId, schedule, projectId, pageId) {
|
|
1214
1316
|
if (tasks.has(jobId))
|
|
1215
|
-
tasks.get(jobId)
|
|
1317
|
+
tasks.get(jobId)?.stop();
|
|
1216
1318
|
const task = import_node_cron.default.schedule(schedule, async () => {
|
|
1217
1319
|
await runJob(db, jobId, projectId, pageId);
|
|
1218
1320
|
});
|
|
@@ -1231,11 +1333,468 @@ async function runJob(db, jobId, projectId, pageId) {
|
|
|
1231
1333
|
perf_score: result.perfScore ?? undefined
|
|
1232
1334
|
});
|
|
1233
1335
|
} catch (err) {
|
|
1234
|
-
finishScanRun(db, run.id, {
|
|
1336
|
+
finishScanRun(db, run.id, {
|
|
1337
|
+
status: "failed",
|
|
1338
|
+
logs_collected: 0,
|
|
1339
|
+
errors_found: 0
|
|
1340
|
+
});
|
|
1235
1341
|
console.error(`Scan failed for page ${page.id}:`, err);
|
|
1236
1342
|
}
|
|
1237
1343
|
}));
|
|
1238
1344
|
updateJob(db, jobId, { last_run_at: new Date().toISOString() });
|
|
1239
1345
|
}
|
|
1240
1346
|
|
|
1241
|
-
|
|
1347
|
+
// src/lib/structured-logs.ts
|
|
1348
|
+
import { createHash as createHash2 } from "crypto";
|
|
1349
|
+
var STRUCTURED_LOG_SOURCES = new Set([
|
|
1350
|
+
"sdk",
|
|
1351
|
+
"script",
|
|
1352
|
+
"scanner",
|
|
1353
|
+
"browser",
|
|
1354
|
+
"node",
|
|
1355
|
+
"bun",
|
|
1356
|
+
"next",
|
|
1357
|
+
"vite",
|
|
1358
|
+
"cli",
|
|
1359
|
+
"build",
|
|
1360
|
+
"test",
|
|
1361
|
+
"mcp",
|
|
1362
|
+
"agent",
|
|
1363
|
+
"otel",
|
|
1364
|
+
"system",
|
|
1365
|
+
"pino",
|
|
1366
|
+
"winston",
|
|
1367
|
+
"structured"
|
|
1368
|
+
]);
|
|
1369
|
+
var PINO_LEVELS = new Map([
|
|
1370
|
+
[10, "debug"],
|
|
1371
|
+
[20, "debug"],
|
|
1372
|
+
[30, "info"],
|
|
1373
|
+
[40, "warn"],
|
|
1374
|
+
[50, "error"],
|
|
1375
|
+
[60, "fatal"]
|
|
1376
|
+
]);
|
|
1377
|
+
var WINSTON_NUMERIC_LEVELS = new Map([
|
|
1378
|
+
[0, "error"],
|
|
1379
|
+
[1, "warn"],
|
|
1380
|
+
[2, "info"],
|
|
1381
|
+
[3, "info"],
|
|
1382
|
+
[4, "debug"],
|
|
1383
|
+
[5, "debug"],
|
|
1384
|
+
[6, "debug"]
|
|
1385
|
+
]);
|
|
1386
|
+
var STRING_LEVELS = new Map([
|
|
1387
|
+
["trace", "debug"],
|
|
1388
|
+
["debug", "debug"],
|
|
1389
|
+
["silly", "debug"],
|
|
1390
|
+
["verbose", "debug"],
|
|
1391
|
+
["http", "info"],
|
|
1392
|
+
["info", "info"],
|
|
1393
|
+
["notice", "info"],
|
|
1394
|
+
["warn", "warn"],
|
|
1395
|
+
["warning", "warn"],
|
|
1396
|
+
["error", "error"],
|
|
1397
|
+
["err", "error"],
|
|
1398
|
+
["fatal", "fatal"],
|
|
1399
|
+
["crit", "fatal"],
|
|
1400
|
+
["critical", "fatal"],
|
|
1401
|
+
["panic", "fatal"]
|
|
1402
|
+
]);
|
|
1403
|
+
function validateStructuredLogReferences(db, entries) {
|
|
1404
|
+
for (let index = 0;index < entries.length; index += 1) {
|
|
1405
|
+
const entry = entries[index];
|
|
1406
|
+
if (!entry)
|
|
1407
|
+
continue;
|
|
1408
|
+
if (entry.project_id && !projectExists(db, entry.project_id)) {
|
|
1409
|
+
throw new Error(`entry[${index}].project_id does not exist`);
|
|
1410
|
+
}
|
|
1411
|
+
if (entry.page_id) {
|
|
1412
|
+
const page = pageProject(db, entry.page_id);
|
|
1413
|
+
if (!page)
|
|
1414
|
+
throw new Error(`entry[${index}].page_id does not exist`);
|
|
1415
|
+
if (entry.project_id && page.project_id !== entry.project_id) {
|
|
1416
|
+
throw new Error(`entry[${index}].page_id does not belong to entry[${index}].project_id`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function structuredLogPayloadToEntries(payload, options = {}) {
|
|
1422
|
+
if (Array.isArray(payload)) {
|
|
1423
|
+
return {
|
|
1424
|
+
entries: payload.map((record, index) => structuredLogToEntry(record, options, { index })),
|
|
1425
|
+
batch: true
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
if (isRecord(payload) && Array.isArray(payload.logs)) {
|
|
1429
|
+
const envelope = payload;
|
|
1430
|
+
const logs = envelope.logs;
|
|
1431
|
+
const envelopeOptions = optionsFromEnvelope(envelope, options);
|
|
1432
|
+
return {
|
|
1433
|
+
entries: logs.map((record, index) => structuredLogToEntry(record, envelopeOptions, { index })),
|
|
1434
|
+
batch: true
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
entries: [structuredLogToEntry(payload, options)],
|
|
1439
|
+
batch: false
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function parseStructuredJsonLines(input, options = {}, source = "jsonl") {
|
|
1443
|
+
const entries = [];
|
|
1444
|
+
const lines = input.split(/\r?\n/);
|
|
1445
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
1446
|
+
const line = lines[index]?.trim();
|
|
1447
|
+
if (!line)
|
|
1448
|
+
continue;
|
|
1449
|
+
let parsed;
|
|
1450
|
+
try {
|
|
1451
|
+
parsed = JSON.parse(line);
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
throw new Error(`line ${index + 1}: invalid JSON (${errorMessage(error)})`);
|
|
1454
|
+
}
|
|
1455
|
+
entries.push(structuredLogToEntry(parsed, options, {
|
|
1456
|
+
index: entries.length,
|
|
1457
|
+
line: index + 1,
|
|
1458
|
+
source
|
|
1459
|
+
}));
|
|
1460
|
+
}
|
|
1461
|
+
return entries;
|
|
1462
|
+
}
|
|
1463
|
+
function ingestStructuredJsonLines(db, input, options = {}, source = "jsonl") {
|
|
1464
|
+
const entries = parseStructuredJsonLines(input, options, source);
|
|
1465
|
+
validateStructuredLogReferences(db, entries);
|
|
1466
|
+
return entries.map((entry) => ingestLog(db, entry));
|
|
1467
|
+
}
|
|
1468
|
+
function structuredLogToEntry(value, options = {}, position = {}) {
|
|
1469
|
+
if (!isRecord(value)) {
|
|
1470
|
+
throw new Error("structured log record must be an object");
|
|
1471
|
+
}
|
|
1472
|
+
const format = detectFormat(value, normalizeFormat(options.format ?? "auto"));
|
|
1473
|
+
const message = messageFromRecord(value);
|
|
1474
|
+
const level = levelFromRecord(value, format);
|
|
1475
|
+
const eventTime = timestampFromRecord(value);
|
|
1476
|
+
const producerEventId = producerId(value);
|
|
1477
|
+
const idRecord = redactedRecordForId(value);
|
|
1478
|
+
const sourceEventId = structuredSourceEventId(idRecord, format, producerEventId, position, options.source_event_prefix);
|
|
1479
|
+
const source = options.source ?? defaultSource(format);
|
|
1480
|
+
assertSupportedSource(source);
|
|
1481
|
+
const service = firstString([
|
|
1482
|
+
options.service,
|
|
1483
|
+
stringValue(value.service),
|
|
1484
|
+
stringPath(value, ["service", "name"]),
|
|
1485
|
+
stringValue(value.name),
|
|
1486
|
+
stringValue(value.logger),
|
|
1487
|
+
stringPath(value, ["logger", "name"])
|
|
1488
|
+
]);
|
|
1489
|
+
const traceId = firstString([
|
|
1490
|
+
options.trace_id,
|
|
1491
|
+
stringValue(value.trace_id),
|
|
1492
|
+
stringValue(value.traceId),
|
|
1493
|
+
stringPath(value, ["trace", "id"]),
|
|
1494
|
+
stringPath(value, ["otel", "trace_id"]),
|
|
1495
|
+
stringValue(value["dd.trace_id"])
|
|
1496
|
+
]);
|
|
1497
|
+
const spanId = firstString([
|
|
1498
|
+
options.span_id,
|
|
1499
|
+
stringValue(value.span_id),
|
|
1500
|
+
stringValue(value.spanId),
|
|
1501
|
+
stringPath(value, ["span", "id"]),
|
|
1502
|
+
stringPath(value, ["otel", "span_id"]),
|
|
1503
|
+
stringValue(value["dd.span_id"])
|
|
1504
|
+
]);
|
|
1505
|
+
const metadata = compactObject({
|
|
1506
|
+
...options.metadata ?? {},
|
|
1507
|
+
structured_log: compactObject({
|
|
1508
|
+
format,
|
|
1509
|
+
source,
|
|
1510
|
+
producer_event_id: redactedProducerId(producerEventId),
|
|
1511
|
+
position: compactObject({ ...position }),
|
|
1512
|
+
level_raw: value.level ?? value.severity ?? value.severityText,
|
|
1513
|
+
time_raw: value.time ?? value.timestamp ?? value["@timestamp"] ?? value.datetime ?? value.date,
|
|
1514
|
+
logger: firstString([
|
|
1515
|
+
stringValue(value.name),
|
|
1516
|
+
stringValue(value.logger),
|
|
1517
|
+
stringPath(value, ["logger", "name"])
|
|
1518
|
+
]),
|
|
1519
|
+
pid: numberOrString(value.pid) ?? numberOrString(isRecord(value.process) ? value.process.pid : undefined),
|
|
1520
|
+
hostname: stringValue(value.hostname) ?? stringPath(value, ["host", "name"]),
|
|
1521
|
+
original: value
|
|
1522
|
+
})
|
|
1523
|
+
});
|
|
1524
|
+
return compactObject({
|
|
1525
|
+
id: sourceEventId ? `log_struct_${stableHash(sourceEventId).slice(0, 32)}` : undefined,
|
|
1526
|
+
timestamp: eventTime,
|
|
1527
|
+
source_event_id: sourceEventId,
|
|
1528
|
+
project_id: options.project_id ?? stringValue(value.project_id),
|
|
1529
|
+
page_id: options.page_id ?? stringValue(value.page_id),
|
|
1530
|
+
level,
|
|
1531
|
+
source,
|
|
1532
|
+
service,
|
|
1533
|
+
message,
|
|
1534
|
+
privacy: options.privacy,
|
|
1535
|
+
machine_id: options.machine_id ?? stringValue(value.machine_id) ?? stringPath(value, ["host", "id"]) ?? stringValue(value.hostname) ?? stringPath(value, ["host", "name"]),
|
|
1536
|
+
repo_id: options.repo_id ?? stringValue(value.repo_id),
|
|
1537
|
+
app_id: options.app_id ?? stringValue(value.app_id) ?? stringPath(value, ["service", "name"]) ?? service,
|
|
1538
|
+
process_id: options.process_id ?? stringValue(value.process_id),
|
|
1539
|
+
run_id: options.run_id ?? stringValue(value.run_id),
|
|
1540
|
+
trace_id: traceId,
|
|
1541
|
+
span_id: spanId,
|
|
1542
|
+
parent_span_id: options.parent_span_id ?? stringValue(value.parent_span_id) ?? stringValue(value.parentSpanId),
|
|
1543
|
+
session_id: options.session_id ?? stringValue(value.session_id) ?? stringValue(value.sessionId),
|
|
1544
|
+
release_id: options.release_id ?? stringValue(value.release_id) ?? stringValue(value.release) ?? stringValue(value.version),
|
|
1545
|
+
environment: options.environment ?? stringValue(value.environment) ?? stringValue(value.env) ?? stringPath(value, ["deployment", "environment"]),
|
|
1546
|
+
agent: options.agent ?? stringValue(value.agent),
|
|
1547
|
+
url: options.url ?? stringValue(value.url) ?? stringPath(value, ["request", "url"]) ?? stringPath(value, ["req", "url"]),
|
|
1548
|
+
stack_trace: stringValue(value.stack) ?? stringPath(value, ["err", "stack"]) ?? stringPath(value, ["error", "stack"]) ?? stringPath(value, ["exception", "stacktrace"]),
|
|
1549
|
+
metadata
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
function optionsFromEnvelope(envelope, defaults) {
|
|
1553
|
+
return compactObject({
|
|
1554
|
+
...defaults,
|
|
1555
|
+
format: formatValue(envelope.format) ?? defaults.format,
|
|
1556
|
+
source: sourceValue(envelope.source) ?? defaults.source,
|
|
1557
|
+
service: stringValue(envelope.service) ?? defaults.service,
|
|
1558
|
+
project_id: stringValue(envelope.project_id) ?? defaults.project_id,
|
|
1559
|
+
page_id: stringValue(envelope.page_id) ?? defaults.page_id,
|
|
1560
|
+
machine_id: stringValue(envelope.machine_id) ?? defaults.machine_id,
|
|
1561
|
+
repo_id: stringValue(envelope.repo_id) ?? defaults.repo_id,
|
|
1562
|
+
app_id: stringValue(envelope.app_id) ?? defaults.app_id,
|
|
1563
|
+
process_id: stringValue(envelope.process_id) ?? defaults.process_id,
|
|
1564
|
+
run_id: stringValue(envelope.run_id) ?? defaults.run_id,
|
|
1565
|
+
trace_id: stringValue(envelope.trace_id) ?? defaults.trace_id,
|
|
1566
|
+
span_id: stringValue(envelope.span_id) ?? defaults.span_id,
|
|
1567
|
+
parent_span_id: stringValue(envelope.parent_span_id) ?? defaults.parent_span_id,
|
|
1568
|
+
session_id: stringValue(envelope.session_id) ?? defaults.session_id,
|
|
1569
|
+
release_id: stringValue(envelope.release_id) ?? defaults.release_id,
|
|
1570
|
+
environment: stringValue(envelope.environment) ?? defaults.environment,
|
|
1571
|
+
agent: stringValue(envelope.agent) ?? defaults.agent,
|
|
1572
|
+
url: stringValue(envelope.url) ?? defaults.url,
|
|
1573
|
+
source_event_prefix: stringValue(envelope.source_event_prefix) ?? defaults.source_event_prefix,
|
|
1574
|
+
metadata: metadataValue(envelope.metadata) ?? defaults.metadata
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
function detectFormat(value, requested) {
|
|
1578
|
+
if (requested !== "auto")
|
|
1579
|
+
return requested;
|
|
1580
|
+
if (typeof value.level === "number" || "msg" in value)
|
|
1581
|
+
return "pino";
|
|
1582
|
+
if (typeof value.level === "string" && "message" in value)
|
|
1583
|
+
return "winston";
|
|
1584
|
+
return "json";
|
|
1585
|
+
}
|
|
1586
|
+
function normalizeFormat(value) {
|
|
1587
|
+
if (value === "auto" || value === "pino" || value === "winston" || value === "json") {
|
|
1588
|
+
return value;
|
|
1589
|
+
}
|
|
1590
|
+
throw new Error("format must be auto, pino, winston, or json");
|
|
1591
|
+
}
|
|
1592
|
+
function defaultSource(format) {
|
|
1593
|
+
if (format === "pino")
|
|
1594
|
+
return "pino";
|
|
1595
|
+
if (format === "winston")
|
|
1596
|
+
return "winston";
|
|
1597
|
+
return "structured";
|
|
1598
|
+
}
|
|
1599
|
+
function levelFromRecord(value, format) {
|
|
1600
|
+
const raw = value.level ?? value.severity ?? value.severityText;
|
|
1601
|
+
if (raw === undefined || raw === null || raw === "")
|
|
1602
|
+
return "info";
|
|
1603
|
+
if (typeof raw === "number") {
|
|
1604
|
+
const mapped = format === "winston" ? WINSTON_NUMERIC_LEVELS.get(raw) : PINO_LEVELS.get(raw) ?? WINSTON_NUMERIC_LEVELS.get(raw);
|
|
1605
|
+
if (mapped)
|
|
1606
|
+
return mapped;
|
|
1607
|
+
throw new Error(`unsupported numeric log level: ${raw}`);
|
|
1608
|
+
}
|
|
1609
|
+
if (typeof raw === "string") {
|
|
1610
|
+
const numeric = Number(raw);
|
|
1611
|
+
if (Number.isFinite(numeric) && raw.trim() !== "") {
|
|
1612
|
+
return levelFromRecord({ level: numeric }, format);
|
|
1613
|
+
}
|
|
1614
|
+
const mapped = STRING_LEVELS.get(raw.toLowerCase());
|
|
1615
|
+
if (mapped)
|
|
1616
|
+
return mapped;
|
|
1617
|
+
}
|
|
1618
|
+
throw new Error(`unsupported log level: ${String(raw)}`);
|
|
1619
|
+
}
|
|
1620
|
+
function messageFromRecord(value) {
|
|
1621
|
+
const raw = value.msg ?? value.message ?? value.body ?? stringPath(value, ["err", "message"]) ?? stringPath(value, ["error", "message"]);
|
|
1622
|
+
const message = messageValue(raw);
|
|
1623
|
+
if (!message)
|
|
1624
|
+
throw new Error("structured log record must include msg or message");
|
|
1625
|
+
return message;
|
|
1626
|
+
}
|
|
1627
|
+
function messageValue(value) {
|
|
1628
|
+
if (typeof value === "string") {
|
|
1629
|
+
const trimmed = value.trim();
|
|
1630
|
+
return trimmed.length > 0 ? value : null;
|
|
1631
|
+
}
|
|
1632
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
1633
|
+
return String(value);
|
|
1634
|
+
}
|
|
1635
|
+
if (value && typeof value === "object") {
|
|
1636
|
+
return stableStringify(value);
|
|
1637
|
+
}
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
function timestampFromRecord(value) {
|
|
1641
|
+
const raw = value.time ?? value.timestamp ?? value["@timestamp"] ?? value.datetime ?? value.date;
|
|
1642
|
+
if (raw === undefined || raw === null || raw === "")
|
|
1643
|
+
return;
|
|
1644
|
+
const timestamp = timestampValue(raw);
|
|
1645
|
+
if (!timestamp)
|
|
1646
|
+
throw new Error(`unsupported timestamp: ${String(raw)}`);
|
|
1647
|
+
return timestamp;
|
|
1648
|
+
}
|
|
1649
|
+
function timestampValue(value) {
|
|
1650
|
+
if (typeof value === "number")
|
|
1651
|
+
return epochNumberToIso(value);
|
|
1652
|
+
if (typeof value === "string") {
|
|
1653
|
+
const trimmed = value.trim();
|
|
1654
|
+
if (!trimmed)
|
|
1655
|
+
return null;
|
|
1656
|
+
const numeric = Number(trimmed);
|
|
1657
|
+
if (Number.isFinite(numeric))
|
|
1658
|
+
return epochNumberToIso(numeric);
|
|
1659
|
+
const parsed = new Date(trimmed);
|
|
1660
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
1661
|
+
return parsed.toISOString();
|
|
1662
|
+
}
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
function epochNumberToIso(value) {
|
|
1666
|
+
if (!Number.isFinite(value))
|
|
1667
|
+
return null;
|
|
1668
|
+
let milliseconds = value;
|
|
1669
|
+
if (value > 10000000000000000) {
|
|
1670
|
+
milliseconds = value / 1e6;
|
|
1671
|
+
} else if (value > 10000000000000) {
|
|
1672
|
+
milliseconds = value / 1000;
|
|
1673
|
+
} else if (value < 10000000000) {
|
|
1674
|
+
milliseconds = value * 1000;
|
|
1675
|
+
}
|
|
1676
|
+
const date = new Date(milliseconds);
|
|
1677
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
1678
|
+
}
|
|
1679
|
+
function producerId(value) {
|
|
1680
|
+
return firstString([
|
|
1681
|
+
stringValue(value.id),
|
|
1682
|
+
stringValue(value.event_id),
|
|
1683
|
+
stringValue(value.eventId),
|
|
1684
|
+
stringValue(value.source_event_id),
|
|
1685
|
+
stringValue(value.log_id),
|
|
1686
|
+
stringValue(value.logId),
|
|
1687
|
+
stringValue(value._open_logs_event_id)
|
|
1688
|
+
]) ?? null;
|
|
1689
|
+
}
|
|
1690
|
+
function structuredSourceEventId(value, format, producerEventId, position, prefix = "structured") {
|
|
1691
|
+
if (producerEventId) {
|
|
1692
|
+
const redacted = redactString(producerEventId, "source_event_id");
|
|
1693
|
+
if (!redacted.report.applied) {
|
|
1694
|
+
return `${prefix}:${format}:producer:${stableHash(redacted.value)}`;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (!hasPosition(position))
|
|
1698
|
+
return;
|
|
1699
|
+
return `${prefix}:${format}:${stableHash({
|
|
1700
|
+
position,
|
|
1701
|
+
timestamp: value.time ?? value.timestamp ?? value["@timestamp"] ?? value.datetime ?? value.date,
|
|
1702
|
+
level: value.level ?? value.severity ?? value.severityText,
|
|
1703
|
+
message: value.msg ?? value.message ?? value.body,
|
|
1704
|
+
service: value.service ?? value.name ?? value.logger,
|
|
1705
|
+
original: value
|
|
1706
|
+
})}`;
|
|
1707
|
+
}
|
|
1708
|
+
function redactedProducerId(value) {
|
|
1709
|
+
return value ? redactString(value, "metadata.structured_log.producer_event_id").value : undefined;
|
|
1710
|
+
}
|
|
1711
|
+
function redactedRecordForId(value) {
|
|
1712
|
+
return redactValue(value, "structured_log_id").value;
|
|
1713
|
+
}
|
|
1714
|
+
function hasPosition(position) {
|
|
1715
|
+
return position.index !== undefined || position.line !== undefined || position.source !== undefined;
|
|
1716
|
+
}
|
|
1717
|
+
function assertSupportedSource(source) {
|
|
1718
|
+
if (!STRUCTURED_LOG_SOURCES.has(source)) {
|
|
1719
|
+
throw new Error(`unsupported structured log source: ${source}`);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function formatValue(value) {
|
|
1723
|
+
if (value === undefined)
|
|
1724
|
+
return;
|
|
1725
|
+
return normalizeFormat(value);
|
|
1726
|
+
}
|
|
1727
|
+
function sourceValue(value) {
|
|
1728
|
+
if (value === undefined)
|
|
1729
|
+
return;
|
|
1730
|
+
const source = stringValue(value);
|
|
1731
|
+
if (!source)
|
|
1732
|
+
throw new Error("source must be a string");
|
|
1733
|
+
assertSupportedSource(source);
|
|
1734
|
+
return source;
|
|
1735
|
+
}
|
|
1736
|
+
function metadataValue(value) {
|
|
1737
|
+
if (value === undefined)
|
|
1738
|
+
return;
|
|
1739
|
+
if (!isRecord(value))
|
|
1740
|
+
throw new Error("metadata must be an object");
|
|
1741
|
+
return value;
|
|
1742
|
+
}
|
|
1743
|
+
function stringValue(value) {
|
|
1744
|
+
if (typeof value === "string" && value.length > 0)
|
|
1745
|
+
return value;
|
|
1746
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
1747
|
+
return String(value);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
function numberOrString(value) {
|
|
1751
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1752
|
+
return value;
|
|
1753
|
+
if (typeof value === "string" && value.length > 0)
|
|
1754
|
+
return value;
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
function stringPath(value, path) {
|
|
1758
|
+
let current = value;
|
|
1759
|
+
for (const part of path) {
|
|
1760
|
+
if (!isRecord(current))
|
|
1761
|
+
return;
|
|
1762
|
+
current = current[part];
|
|
1763
|
+
}
|
|
1764
|
+
return stringValue(current);
|
|
1765
|
+
}
|
|
1766
|
+
function firstString(values) {
|
|
1767
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
1768
|
+
}
|
|
1769
|
+
function compactObject(value) {
|
|
1770
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
1771
|
+
}
|
|
1772
|
+
function stableHash(value) {
|
|
1773
|
+
return createHash2("sha256").update(stableStringify(value)).digest("hex");
|
|
1774
|
+
}
|
|
1775
|
+
function stableStringify(value) {
|
|
1776
|
+
if (value === undefined)
|
|
1777
|
+
return "undefined";
|
|
1778
|
+
if (typeof value === "bigint")
|
|
1779
|
+
return JSON.stringify(value.toString());
|
|
1780
|
+
if (value === null || typeof value !== "object")
|
|
1781
|
+
return JSON.stringify(value);
|
|
1782
|
+
if (Array.isArray(value))
|
|
1783
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
1784
|
+
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
|
|
1785
|
+
}
|
|
1786
|
+
function isRecord(value) {
|
|
1787
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1788
|
+
}
|
|
1789
|
+
function errorMessage(error) {
|
|
1790
|
+
return error instanceof Error ? error.message : String(error);
|
|
1791
|
+
}
|
|
1792
|
+
function projectExists(db, projectId) {
|
|
1793
|
+
const row = db.prepare("SELECT id FROM projects WHERE id = ?").get(projectId);
|
|
1794
|
+
return Boolean(row);
|
|
1795
|
+
}
|
|
1796
|
+
function pageProject(db, pageId) {
|
|
1797
|
+
return db.prepare("SELECT project_id FROM pages WHERE id = ?").get(pageId);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
export { runRetentionForProject, setRetentionPolicy, setPageAuth, deletePageAuth, startScheduler, runJob, validateStructuredLogReferences, structuredLogPayloadToEntries, ingestStructuredJsonLines, structuredLogToEntry };
|