@hasna/logs 0.3.24 → 0.3.26
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/LICENSE +2 -1
- package/README.md +22 -3
- package/bun.lock +15 -7
- package/dist/cli/index.js +688 -3
- package/dist/http-zm3ph78w.js +1240 -0
- package/dist/index-5qznfyah.js +5868 -0
- package/dist/{index-pf8hpweg.js → index-gc0zvs88.js} +2 -2
- package/dist/index-pen6t0yc.js +10794 -0
- package/dist/mcp/index.js +6618 -22153
- package/dist/server/index.js +2 -2
- package/package.json +4 -4
- package/sdk/package.json +9 -4
- package/src/cli/entrypoints.test.ts +1 -1
- package/src/cli/index.ts +2 -0
- package/src/db/index.ts +1 -1
- package/src/lib/ingest.ts +3 -3
- package/src/mcp/http.test.ts +92 -0
- package/src/mcp/http.ts +135 -0
- package/src/mcp/index.ts +34 -30
- package/dist/index-75dwghvv.js +0 -625
- package/dist/index-g8f8kep6.js +0 -625
- package/dist/index-w24zm361.js +0 -1241
- package/src/lib/cloud-sync.ts +0 -167
- package/src/lib/remote-storage.ts +0 -45
package/src/lib/cloud-sync.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite"
|
|
2
|
-
import { getDb } from "../db/index.ts"
|
|
3
|
-
import { PG_MIGRATIONS } from "../db/pg-migrations.ts"
|
|
4
|
-
import { PgAdapterAsync } from "./remote-storage.ts"
|
|
5
|
-
|
|
6
|
-
export const CLOUD_TABLES = [
|
|
7
|
-
"projects",
|
|
8
|
-
"pages",
|
|
9
|
-
"logs",
|
|
10
|
-
"scan_jobs",
|
|
11
|
-
"scan_runs",
|
|
12
|
-
"performance_snapshots",
|
|
13
|
-
"alert_rules",
|
|
14
|
-
"issues",
|
|
15
|
-
"page_auth",
|
|
16
|
-
"feedback",
|
|
17
|
-
] as const
|
|
18
|
-
|
|
19
|
-
type Row = Record<string, unknown>
|
|
20
|
-
|
|
21
|
-
export function getCloudDatabaseUrl(): string | null {
|
|
22
|
-
return process.env.HASNA_LOGS_CLOUD_DATABASE_URL
|
|
23
|
-
?? process.env.LOGS_CLOUD_DATABASE_URL
|
|
24
|
-
?? null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function getCloudPg(): Promise<PgAdapterAsync> {
|
|
28
|
-
const url = getCloudDatabaseUrl()
|
|
29
|
-
if (!url) throw new Error("Missing HASNA_LOGS_CLOUD_DATABASE_URL or LOGS_CLOUD_DATABASE_URL")
|
|
30
|
-
return new PgAdapterAsync(url)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function runCloudMigrations(remote: PgAdapterAsync): Promise<void> {
|
|
34
|
-
for (const sql of PG_MIGRATIONS) await remote.run(sql)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function cloudPush(opts?: { tables?: string[] }): Promise<{ rows: number }> {
|
|
38
|
-
const remote = await getCloudPg()
|
|
39
|
-
try {
|
|
40
|
-
await runCloudMigrations(remote)
|
|
41
|
-
const db = getDb()
|
|
42
|
-
let rows = 0
|
|
43
|
-
for (const table of resolveTables(opts?.tables)) rows += await pushTable(db, remote, table)
|
|
44
|
-
return { rows }
|
|
45
|
-
} finally {
|
|
46
|
-
await remote.close()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function cloudPull(opts?: { tables?: string[] }): Promise<{ rows: number }> {
|
|
51
|
-
const remote = await getCloudPg()
|
|
52
|
-
try {
|
|
53
|
-
await runCloudMigrations(remote)
|
|
54
|
-
const db = getDb()
|
|
55
|
-
let rows = 0
|
|
56
|
-
for (const table of resolveTables(opts?.tables)) rows += await pullTable(remote, db, table)
|
|
57
|
-
return { rows }
|
|
58
|
-
} finally {
|
|
59
|
-
await remote.close()
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function cloudSync(opts?: { tables?: string[] }): Promise<{ push: number; pull: number }> {
|
|
64
|
-
const push = await cloudPush(opts)
|
|
65
|
-
const pull = await cloudPull(opts)
|
|
66
|
-
return { push: push.rows, pull: pull.rows }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function resolveTables(tables?: string[]): string[] {
|
|
70
|
-
if (!tables || tables.length === 0) return [...CLOUD_TABLES]
|
|
71
|
-
const allowed = new Set<string>(CLOUD_TABLES)
|
|
72
|
-
const requested = tables.map(table => table.trim()).filter(Boolean)
|
|
73
|
-
const invalid = requested.filter(table => !allowed.has(table))
|
|
74
|
-
if (invalid.length > 0) throw new Error(`Unknown logs sync table(s): ${invalid.join(", ")}`)
|
|
75
|
-
return requested
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function quoteIdent(identifier: string): string {
|
|
79
|
-
return `"${identifier.replace(/"/g, "\"\"")}"`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function pushTable(db: Database, remote: PgAdapterAsync, table: string): Promise<number> {
|
|
83
|
-
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all() as Row[]
|
|
84
|
-
if (rows.length === 0) return 0
|
|
85
|
-
const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]!))
|
|
86
|
-
await upsertPg(remote, table, columns, rows)
|
|
87
|
-
return rows.length
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function pullTable(remote: PgAdapterAsync, db: Database, table: string): Promise<number> {
|
|
91
|
-
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`) as Row[]
|
|
92
|
-
if (rows.length === 0) return 0
|
|
93
|
-
const columns = filterLocalColumns(db, table, Object.keys(rows[0]!))
|
|
94
|
-
upsertSqlite(db, table, columns, rows)
|
|
95
|
-
return rows.length
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function filterRemoteColumns(remote: PgAdapterAsync, table: string, columns: string[]): Promise<string[]> {
|
|
99
|
-
const rows = await remote.all(`
|
|
100
|
-
SELECT column_name
|
|
101
|
-
FROM information_schema.columns
|
|
102
|
-
WHERE table_schema = 'public' AND table_name = ?
|
|
103
|
-
`, table) as Array<{ column_name: string }>
|
|
104
|
-
if (rows.length === 0) return columns
|
|
105
|
-
const allowed = new Set(rows.map(row => row.column_name))
|
|
106
|
-
return columns.filter(column => allowed.has(column))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function filterLocalColumns(db: Database, table: string, columns: string[]): string[] {
|
|
110
|
-
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all() as Array<{ name: string }>
|
|
111
|
-
const allowed = new Set(rows.map(row => row.name))
|
|
112
|
-
return columns.filter(column => allowed.has(column))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function upsertPg(remote: PgAdapterAsync, table: string, columns: string[], rows: Row[]): Promise<void> {
|
|
116
|
-
if (columns.length === 0) return
|
|
117
|
-
const columnList = columns.map(quoteIdent).join(", ")
|
|
118
|
-
const updateColumns = columns.filter(column => column !== "id")
|
|
119
|
-
const setClause = updateColumns.length > 0
|
|
120
|
-
? updateColumns.map(column => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ")
|
|
121
|
-
: `${quoteIdent("id")} = EXCLUDED.${quoteIdent("id")}`
|
|
122
|
-
|
|
123
|
-
for (const row of rows) {
|
|
124
|
-
const placeholders = columns.map((_, index) => `$${index + 1}`).join(", ")
|
|
125
|
-
const params = columns.map(column => coerceForPg(table, column, row[column]))
|
|
126
|
-
await remote.run(
|
|
127
|
-
`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
128
|
-
ON CONFLICT (${quoteIdent("id")}) DO UPDATE SET ${setClause}`,
|
|
129
|
-
...params,
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function upsertSqlite(db: Database, table: string, columns: string[], rows: Row[]): void {
|
|
135
|
-
if (columns.length === 0) return
|
|
136
|
-
const columnList = columns.map(quoteIdent).join(", ")
|
|
137
|
-
const placeholders = columns.map(() => "?").join(", ")
|
|
138
|
-
const updateColumns = columns.filter(column => column !== "id")
|
|
139
|
-
const setClause = updateColumns.length > 0
|
|
140
|
-
? updateColumns.map(column => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ")
|
|
141
|
-
: `${quoteIdent("id")} = excluded.${quoteIdent("id")}`
|
|
142
|
-
const statement = db.prepare(
|
|
143
|
-
`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
|
|
144
|
-
ON CONFLICT (${quoteIdent("id")}) DO UPDATE SET ${setClause}`,
|
|
145
|
-
)
|
|
146
|
-
const insert = db.transaction((batch: Row[]) => {
|
|
147
|
-
for (const row of batch) statement.run(...columns.map(column => coerceForSqlite(row[column])))
|
|
148
|
-
})
|
|
149
|
-
insert(rows)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function coerceForPg(table: string, column: string, value: unknown): unknown {
|
|
153
|
-
if (value === undefined) return null
|
|
154
|
-
if ((table === "scan_jobs" || table === "alert_rules") && column === "enabled") {
|
|
155
|
-
return Boolean(value)
|
|
156
|
-
}
|
|
157
|
-
return value
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function coerceForSqlite(value: unknown): string | number | bigint | boolean | null | Uint8Array {
|
|
161
|
-
if (value === undefined || value === null) return null
|
|
162
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return value
|
|
163
|
-
if (value instanceof Date) return value.toISOString()
|
|
164
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return value
|
|
165
|
-
if (typeof value === "object") return JSON.stringify(value)
|
|
166
|
-
return String(value)
|
|
167
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import pg from "pg"
|
|
2
|
-
import type { Pool } from "pg"
|
|
3
|
-
|
|
4
|
-
function translatePlaceholders(sql: string): string {
|
|
5
|
-
let index = 0
|
|
6
|
-
return sql.replace(/\?/g, () => `$${++index}`)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function normalizeParams(params: unknown[]): unknown[] {
|
|
10
|
-
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params
|
|
11
|
-
return flat.map(value => value === undefined ? null : value)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function sslConfigFor(connectionString: string): { rejectUnauthorized: boolean } | undefined {
|
|
15
|
-
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true")
|
|
16
|
-
? { rejectUnauthorized: false }
|
|
17
|
-
: undefined
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class PgAdapterAsync {
|
|
21
|
-
private readonly pool: Pool
|
|
22
|
-
|
|
23
|
-
constructor(connectionString: string) {
|
|
24
|
-
this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) })
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async run(sql: string, ...params: unknown[]): Promise<{ changes: number }> {
|
|
28
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params))
|
|
29
|
-
return { changes: result.rowCount ?? 0 }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async get(sql: string, ...params: unknown[]): Promise<unknown> {
|
|
33
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params))
|
|
34
|
-
return result.rows[0] ?? null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async all(sql: string, ...params: unknown[]): Promise<unknown[]> {
|
|
38
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params))
|
|
39
|
-
return result.rows
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async close(): Promise<void> {
|
|
43
|
-
await this.pool.end()
|
|
44
|
-
}
|
|
45
|
-
}
|