orez 0.0.46 → 0.0.47
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 +4 -8
- package/dist/admin/http-proxy.d.ts +31 -0
- package/dist/admin/http-proxy.d.ts.map +1 -0
- package/dist/admin/http-proxy.js +140 -0
- package/dist/admin/http-proxy.js.map +1 -0
- package/dist/admin/log-store.d.ts +22 -0
- package/dist/admin/log-store.d.ts.map +1 -0
- package/dist/admin/log-store.js +86 -0
- package/dist/admin/log-store.js.map +1 -0
- package/dist/admin/server.d.ts +19 -0
- package/dist/admin/server.d.ts.map +1 -0
- package/dist/admin/server.js +110 -0
- package/dist/admin/server.js.map +1 -0
- package/dist/admin/ui.d.ts +2 -0
- package/dist/admin/ui.d.ts.map +1 -0
- package/dist/admin/ui.js +683 -0
- package/dist/admin/ui.js.map +1 -0
- package/dist/cli.js +48 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -20
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +9 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +24 -1
- package/dist/log.js.map +1 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +19 -4
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +1 -0
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +1 -1
- package/dist/pglite-manager.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +20 -2
- package/dist/replication/handler.js.map +1 -1
- package/dist/vite-plugin.d.ts +3 -0
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +24 -0
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +4 -2
- package/src/admin/http-proxy.ts +186 -0
- package/src/admin/log-store.ts +111 -0
- package/src/admin/server.ts +148 -0
- package/src/admin/ui.ts +682 -0
- package/src/cli.ts +49 -1
- package/src/config.ts +8 -0
- package/src/index.ts +192 -20
- package/src/log.ts +25 -1
- package/src/pg-proxy.ts +26 -6
- package/src/pglite-manager.ts +1 -1
- package/src/replication/handler.ts +21 -2
- package/src/vite-plugin.ts +28 -0
package/dist/vite-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAc1C,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,OAA2B;IACtD,IAAI,IAAI,GAAiC,IAAI,CAAA;IAC7C,IAAI,QAAQ,GAAkB,IAAI,CAAA;IAClC,IAAI,WAAW,GAAkB,IAAI,CAAA;IAErC,OAAO;QACL,IAAI,EAAE,MAAM;QAEZ,KAAK,CAAC,eAAe,CAAC,MAAM;YAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC5B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;YAC3C,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;YAElB,IAAI,OAAO,EAAE,EAAE,EAAE,CAAC;gBAChB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;gBACtD,QAAQ,GAAG,MAAM,YAAY,CAAC;oBAC5B,IAAI,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI;oBAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO;iBAC/B,CAAC,CAAA;YACJ,CAAC;YAED,IAAI,OAAO,EAAE,KAAK,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACtC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;gBAC9C,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;gBACxC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAA;gBACnE,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,CAAA;gBAC9C,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAA;gBAC9D,WAAW,GAAG,MAAM,gBAAgB,CAAC;oBACnC,IAAI,EAAE,YAAY;oBAClB,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,SAAS;oBACT,OAAO,EAAE,MAAM,CAAC,YAAY,IAAI,SAAS;iBAC1C,CAAC,CAAA;gBACF,GAAG,CAAC,IAAI,CAAC,2BAA2B,YAAY,EAAE,CAAC,CAAA;gBACnD,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;oBAC5B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;oBAC7C,GAAG,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;gBACzE,CAAC;YACH,CAAC;YAED,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACxC,WAAW,EAAE,KAAK,EAAE,CAAA;gBACpB,QAAQ,EAAE,KAAK,EAAE,CAAA;gBACjB,IAAI,IAAI,EAAE,CAAC;oBACT,MAAM,IAAI,EAAE,CAAA;oBACZ,IAAI,GAAG,IAAI,CAAA;gBACb,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"description": "PGlite-powered zero-sync development backend. No Docker required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,13 +40,15 @@
|
|
|
40
40
|
"check": "tsc --noEmit",
|
|
41
41
|
"demo:test": "bun demo/src/test/run-e2e.ts",
|
|
42
42
|
"check:all": "bun run lint && bun run format:check && bun run check && bun run test",
|
|
43
|
+
"test:chat": "bun scripts/test-chat-integration.ts",
|
|
44
|
+
"test:chat:smoke": "bun scripts/test-chat-integration.ts --smoke",
|
|
43
45
|
"release": "bun scripts/release.ts"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"@electric-sql/pglite": "^0.2.17",
|
|
47
49
|
"@electric-sql/pglite-tools": "0.2.4",
|
|
48
50
|
"@rocicorp/zero": ">=0.1.0",
|
|
49
|
-
"bedrock-sqlite": "0.0.
|
|
51
|
+
"bedrock-sqlite": "0.0.40",
|
|
50
52
|
"citty": "^0.2.0",
|
|
51
53
|
"postgres": "^3.4.5",
|
|
52
54
|
"pgsql-parser": "^17.9.11"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createServer,
|
|
3
|
+
request as httpRequest,
|
|
4
|
+
type Server,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type ServerResponse,
|
|
7
|
+
} from 'node:http'
|
|
8
|
+
import type { Socket } from 'node:net'
|
|
9
|
+
|
|
10
|
+
export interface HttpLogEntry {
|
|
11
|
+
id: number
|
|
12
|
+
ts: number
|
|
13
|
+
method: string
|
|
14
|
+
path: string
|
|
15
|
+
status: number
|
|
16
|
+
duration: number
|
|
17
|
+
reqSize: number
|
|
18
|
+
resSize: number
|
|
19
|
+
reqHeaders: Record<string, string>
|
|
20
|
+
resHeaders: Record<string, string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HttpLogStore {
|
|
24
|
+
push(entry: Omit<HttpLogEntry, 'id'>): void
|
|
25
|
+
query(opts?: { since?: number; path?: string }): { entries: HttpLogEntry[]; cursor: number }
|
|
26
|
+
clear(): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MAX_ENTRIES = 10_000
|
|
30
|
+
|
|
31
|
+
export function createHttpLogStore(): HttpLogStore {
|
|
32
|
+
const entries: HttpLogEntry[] = []
|
|
33
|
+
let nextId = 1
|
|
34
|
+
|
|
35
|
+
function push(entry: Omit<HttpLogEntry, 'id'>) {
|
|
36
|
+
const full: HttpLogEntry = { ...entry, id: nextId++ }
|
|
37
|
+
entries.push(full)
|
|
38
|
+
if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function query(opts?: { since?: number; path?: string }) {
|
|
42
|
+
let result: HttpLogEntry[] = entries
|
|
43
|
+
if (opts?.since) {
|
|
44
|
+
const since = opts.since
|
|
45
|
+
let lo = 0
|
|
46
|
+
let hi = result.length
|
|
47
|
+
while (lo < hi) {
|
|
48
|
+
const mid = (lo + hi) >>> 1
|
|
49
|
+
if (result[mid].id <= since) lo = mid + 1
|
|
50
|
+
else hi = mid
|
|
51
|
+
}
|
|
52
|
+
result = result.slice(lo)
|
|
53
|
+
}
|
|
54
|
+
if (opts?.path) {
|
|
55
|
+
const p = opts.path
|
|
56
|
+
result = result.filter((e) => e.path.includes(p))
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
entries: result,
|
|
60
|
+
cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clear() {
|
|
65
|
+
entries.length = 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { push, query, clear }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function flatHeaders(headers: Record<string, any>): Record<string, string> {
|
|
72
|
+
const out: Record<string, string> = {}
|
|
73
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
74
|
+
out[k] = Array.isArray(v) ? v.join(', ') : String(v ?? '')
|
|
75
|
+
}
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function startHttpProxy(opts: {
|
|
80
|
+
listenPort: number
|
|
81
|
+
targetPort: number
|
|
82
|
+
httpLog: HttpLogStore
|
|
83
|
+
}): Promise<Server> {
|
|
84
|
+
const { listenPort, targetPort, httpLog } = opts
|
|
85
|
+
|
|
86
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
87
|
+
const start = Date.now()
|
|
88
|
+
let reqSize = 0
|
|
89
|
+
const reqChunks: Buffer[] = []
|
|
90
|
+
|
|
91
|
+
req.on('data', (chunk: Buffer) => {
|
|
92
|
+
reqSize += chunk.length
|
|
93
|
+
reqChunks.push(chunk)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
req.on('end', () => {
|
|
97
|
+
const proxyReq = httpRequest(
|
|
98
|
+
{
|
|
99
|
+
hostname: '127.0.0.1',
|
|
100
|
+
port: targetPort,
|
|
101
|
+
path: req.url,
|
|
102
|
+
method: req.method,
|
|
103
|
+
headers: req.headers,
|
|
104
|
+
},
|
|
105
|
+
(proxyRes) => {
|
|
106
|
+
let resSize = 0
|
|
107
|
+
proxyRes.on('data', (chunk: Buffer) => {
|
|
108
|
+
resSize += chunk.length
|
|
109
|
+
})
|
|
110
|
+
proxyRes.on('end', () => {
|
|
111
|
+
httpLog.push({
|
|
112
|
+
ts: start,
|
|
113
|
+
method: req.method || 'GET',
|
|
114
|
+
path: req.url || '/',
|
|
115
|
+
status: proxyRes.statusCode || 0,
|
|
116
|
+
duration: Date.now() - start,
|
|
117
|
+
reqSize,
|
|
118
|
+
resSize,
|
|
119
|
+
reqHeaders: flatHeaders(req.headers),
|
|
120
|
+
resHeaders: flatHeaders(proxyRes.headers),
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers)
|
|
124
|
+
proxyRes.pipe(res)
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
proxyReq.on('error', (err) => {
|
|
129
|
+
res.writeHead(502)
|
|
130
|
+
res.end('proxy error: ' + err.message)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
for (const chunk of reqChunks) proxyReq.write(chunk)
|
|
134
|
+
proxyReq.end()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// websocket upgrade passthrough
|
|
139
|
+
server.on('upgrade', (req: IncomingMessage, socket: Socket, head: Buffer) => {
|
|
140
|
+
const start = Date.now()
|
|
141
|
+
const proxyReq = httpRequest({
|
|
142
|
+
hostname: '127.0.0.1',
|
|
143
|
+
port: targetPort,
|
|
144
|
+
path: req.url,
|
|
145
|
+
method: req.method,
|
|
146
|
+
headers: req.headers,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
|
|
150
|
+
httpLog.push({
|
|
151
|
+
ts: start,
|
|
152
|
+
method: 'WS',
|
|
153
|
+
path: req.url || '/',
|
|
154
|
+
status: 101,
|
|
155
|
+
duration: Date.now() - start,
|
|
156
|
+
reqSize: 0,
|
|
157
|
+
resSize: 0,
|
|
158
|
+
reqHeaders: flatHeaders(req.headers),
|
|
159
|
+
resHeaders: flatHeaders(proxyRes.headers),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// forward the 101 response
|
|
163
|
+
let rawHeaders = 'HTTP/1.1 101 Switching Protocols\r\n'
|
|
164
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
165
|
+
rawHeaders += `${k}: ${Array.isArray(v) ? v.join(', ') : v}\r\n`
|
|
166
|
+
}
|
|
167
|
+
rawHeaders += '\r\n'
|
|
168
|
+
socket.write(rawHeaders)
|
|
169
|
+
if (proxyHead.length) socket.write(proxyHead)
|
|
170
|
+
|
|
171
|
+
proxySocket.pipe(socket)
|
|
172
|
+
socket.pipe(proxySocket)
|
|
173
|
+
proxySocket.on('error', () => socket.destroy())
|
|
174
|
+
socket.on('error', () => proxySocket.destroy())
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
proxyReq.on('error', () => socket.destroy())
|
|
178
|
+
proxyReq.write(head)
|
|
179
|
+
proxyReq.end()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
server.listen(listenPort, '127.0.0.1', () => resolve(server))
|
|
184
|
+
server.on('error', reject)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, appendFileSync, statSync, renameSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
id: number
|
|
6
|
+
ts: number
|
|
7
|
+
source: string
|
|
8
|
+
level: string
|
|
9
|
+
msg: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LogStore {
|
|
13
|
+
push(source: string, level: string, msg: string): void
|
|
14
|
+
query(opts?: { source?: string; level?: string; since?: number }): {
|
|
15
|
+
entries: LogEntry[]
|
|
16
|
+
cursor: number
|
|
17
|
+
}
|
|
18
|
+
getAll(): LogEntry[]
|
|
19
|
+
clear(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g
|
|
23
|
+
const MAX_ENTRIES = 50_000
|
|
24
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024
|
|
25
|
+
const LEVEL_PRIORITY: Record<string, number> = { error: 0, warn: 1, info: 2, debug: 3 }
|
|
26
|
+
|
|
27
|
+
export function createLogStore(dataDir: string, writeToDisk = true): LogStore {
|
|
28
|
+
const entries: LogEntry[] = []
|
|
29
|
+
let nextId = 1
|
|
30
|
+
|
|
31
|
+
const logsDir = join(dataDir, 'logs')
|
|
32
|
+
const logFile = join(logsDir, 'orez.log')
|
|
33
|
+
const backupFile = join(logsDir, 'orez.log.1')
|
|
34
|
+
|
|
35
|
+
if (writeToDisk) {
|
|
36
|
+
mkdirSync(logsDir, { recursive: true })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rotateIfNeeded() {
|
|
40
|
+
if (!writeToDisk) return
|
|
41
|
+
try {
|
|
42
|
+
if (!existsSync(logFile)) return
|
|
43
|
+
const stat = statSync(logFile)
|
|
44
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
45
|
+
renameSync(logFile, backupFile)
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function push(source: string, level: string, msg: string) {
|
|
51
|
+
const entry: LogEntry = {
|
|
52
|
+
id: nextId++,
|
|
53
|
+
ts: Date.now(),
|
|
54
|
+
source,
|
|
55
|
+
level,
|
|
56
|
+
msg: msg.replace(ANSI_RE, ''),
|
|
57
|
+
}
|
|
58
|
+
entries.push(entry)
|
|
59
|
+
if (entries.length > MAX_ENTRIES) {
|
|
60
|
+
entries.splice(0, entries.length - MAX_ENTRIES)
|
|
61
|
+
}
|
|
62
|
+
if (writeToDisk) {
|
|
63
|
+
try {
|
|
64
|
+
const ts = new Date(entry.ts).toISOString()
|
|
65
|
+
appendFileSync(logFile, '[' + ts + '] [' + source + '] [' + level + '] ' + entry.msg + '\n')
|
|
66
|
+
rotateIfNeeded()
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function query(opts?: { source?: string; level?: string; since?: number }) {
|
|
72
|
+
let result = entries
|
|
73
|
+
|
|
74
|
+
if (opts?.since) {
|
|
75
|
+
const since = opts.since
|
|
76
|
+
let lo = 0
|
|
77
|
+
let hi = result.length
|
|
78
|
+
while (lo < hi) {
|
|
79
|
+
const mid = (lo + hi) >>> 1
|
|
80
|
+
if (result[mid].id <= since) lo = mid + 1
|
|
81
|
+
else hi = mid
|
|
82
|
+
}
|
|
83
|
+
result = result.slice(lo)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (opts?.source) {
|
|
87
|
+
const source = opts.source
|
|
88
|
+
result = result.filter((e) => e.source === source)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (opts?.level) {
|
|
92
|
+
const maxPriority = LEVEL_PRIORITY[opts.level] ?? 3
|
|
93
|
+
result = result.filter((e) => (LEVEL_PRIORITY[e.level] ?? 3) <= maxPriority)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
entries: result,
|
|
98
|
+
cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getAll() {
|
|
103
|
+
return [...entries]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clear() {
|
|
107
|
+
entries.length = 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { push, query, getAll, clear }
|
|
111
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createServer,
|
|
3
|
+
type Server,
|
|
4
|
+
type IncomingMessage,
|
|
5
|
+
type ServerResponse,
|
|
6
|
+
} from 'node:http'
|
|
7
|
+
import { log } from '../log.js'
|
|
8
|
+
import { getAdminHtml } from './ui.js'
|
|
9
|
+
import type { LogStore } from './log-store.js'
|
|
10
|
+
import type { HttpLogStore } from './http-proxy.js'
|
|
11
|
+
import type { ZeroLiteConfig } from '../config.js'
|
|
12
|
+
|
|
13
|
+
export interface AdminActions {
|
|
14
|
+
restartZero?: () => Promise<void>
|
|
15
|
+
resetZero?: () => Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AdminServerOpts {
|
|
19
|
+
port: number
|
|
20
|
+
logStore: LogStore
|
|
21
|
+
config: ZeroLiteConfig
|
|
22
|
+
zeroEnv: Record<string, string>
|
|
23
|
+
actions?: AdminActions
|
|
24
|
+
startTime: number
|
|
25
|
+
httpLog?: HttpLogStore
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function corsHeaders(): Record<string, string> {
|
|
29
|
+
return {
|
|
30
|
+
'Access-Control-Allow-Origin': '*',
|
|
31
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
32
|
+
'Access-Control-Allow-Headers': '*',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function json(res: ServerResponse, data: unknown, status = 200) {
|
|
37
|
+
const headers = { ...corsHeaders(), 'Content-Type': 'application/json' }
|
|
38
|
+
res.writeHead(status, headers)
|
|
39
|
+
res.end(JSON.stringify(data))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function startAdminServer(opts: AdminServerOpts): Promise<Server> {
|
|
43
|
+
const { logStore, config, zeroEnv, actions, startTime } = opts
|
|
44
|
+
const html = getAdminHtml()
|
|
45
|
+
|
|
46
|
+
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
47
|
+
const headers = corsHeaders()
|
|
48
|
+
|
|
49
|
+
if (req.method === 'OPTIONS') {
|
|
50
|
+
res.writeHead(200, headers)
|
|
51
|
+
res.end()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const url = new URL(req.url || '/', 'http://localhost:' + opts.port)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (req.method === 'GET' && url.pathname === '/') {
|
|
59
|
+
res.writeHead(200, { ...headers, 'Content-Type': 'text/html' })
|
|
60
|
+
res.end(html)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (req.method === 'GET' && url.pathname === '/api/logs') {
|
|
65
|
+
const source = url.searchParams.get('source') || undefined
|
|
66
|
+
const level = url.searchParams.get('level') || undefined
|
|
67
|
+
const sinceStr = url.searchParams.get('since')
|
|
68
|
+
const since = sinceStr ? Number(sinceStr) : undefined
|
|
69
|
+
json(res, logStore.query({ source, level, since }))
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (req.method === 'GET' && url.pathname === '/api/env') {
|
|
74
|
+
const filtered = Object.entries(zeroEnv)
|
|
75
|
+
.filter(([k]) => k.startsWith('ZERO_') || k === 'NODE_ENV' || k === 'NODE_OPTIONS')
|
|
76
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
77
|
+
json(res, { env: Object.fromEntries(filtered) })
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
82
|
+
json(res, {
|
|
83
|
+
pgPort: config.pgPort,
|
|
84
|
+
zeroPort: config.zeroPort,
|
|
85
|
+
adminPort: opts.port,
|
|
86
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
87
|
+
logLevel: config.logLevel,
|
|
88
|
+
skipZeroCache: config.skipZeroCache,
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (req.method === 'POST' && url.pathname === '/api/actions/restart-zero') {
|
|
94
|
+
if (!actions?.restartZero) {
|
|
95
|
+
json(res, { ok: false, message: 'zero-cache not running' }, 400)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
log.orez('admin: restarting zero-cache')
|
|
99
|
+
await actions.restartZero()
|
|
100
|
+
json(res, { ok: true, message: 'zero-cache restarted' })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (req.method === 'POST' && url.pathname === '/api/actions/reset-zero') {
|
|
105
|
+
if (!actions?.resetZero) {
|
|
106
|
+
json(res, { ok: false, message: 'zero-cache not running' }, 400)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
log.orez('admin: resetting zero-cache')
|
|
110
|
+
await actions.resetZero()
|
|
111
|
+
json(res, { ok: true, message: 'zero-cache reset and restarted' })
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (req.method === 'POST' && url.pathname === '/api/actions/clear-logs') {
|
|
116
|
+
logStore.clear()
|
|
117
|
+
json(res, { ok: true, message: 'logs cleared' })
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (req.method === 'GET' && url.pathname === '/api/http-log') {
|
|
122
|
+
const sinceStr = url.searchParams.get('since')
|
|
123
|
+
const path = url.searchParams.get('path') || undefined
|
|
124
|
+
const since = sinceStr ? Number(sinceStr) : undefined
|
|
125
|
+
json(res, opts.httpLog?.query({ since, path }) || { entries: [], cursor: 0 })
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (req.method === 'POST' && url.pathname === '/api/actions/clear-http') {
|
|
130
|
+
opts.httpLog?.clear()
|
|
131
|
+
json(res, { ok: true, message: 'http log cleared' })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
res.writeHead(404, headers)
|
|
136
|
+
res.end('not found')
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
json(res, { error: err?.message ?? 'internal error' }, 500)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
server.listen(opts.port, '127.0.0.1', () => {
|
|
144
|
+
resolve(server)
|
|
145
|
+
})
|
|
146
|
+
server.on('error', reject)
|
|
147
|
+
})
|
|
148
|
+
}
|