agent-message 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.
@@ -0,0 +1,175 @@
1
+ import { createReadStream } from 'node:fs'
2
+ import { access, readFile, stat } from 'node:fs/promises'
3
+ import http from 'node:http'
4
+ import { basename, extname, join, normalize, resolve } from 'node:path'
5
+
6
+ const host = process.env.AGENT_GATEWAY_HOST ?? '127.0.0.1'
7
+ const port = Number(process.env.AGENT_GATEWAY_PORT ?? '8788')
8
+ const apiOrigin = process.env.AGENT_API_ORIGIN ?? 'http://127.0.0.1:18080'
9
+ const distDir = resolve(process.env.AGENT_WEB_DIST ?? join(process.cwd(), 'web', 'dist'))
10
+ const indexPath = join(distDir, 'index.html')
11
+
12
+ const contentTypes = new Map([
13
+ ['.css', 'text/css; charset=utf-8'],
14
+ ['.html', 'text/html; charset=utf-8'],
15
+ ['.ico', 'image/x-icon'],
16
+ ['.js', 'text/javascript; charset=utf-8'],
17
+ ['.json', 'application/json; charset=utf-8'],
18
+ ['.mjs', 'text/javascript; charset=utf-8'],
19
+ ['.png', 'image/png'],
20
+ ['.svg', 'image/svg+xml'],
21
+ ['.txt', 'text/plain; charset=utf-8'],
22
+ ['.webmanifest', 'application/manifest+json; charset=utf-8'],
23
+ ['.woff2', 'font/woff2'],
24
+ ])
25
+
26
+ function resolveCacheHeaders(path) {
27
+ const fileName = basename(path)
28
+
29
+ if (fileName === 'sw.js') {
30
+ return {
31
+ 'cache-control': 'no-cache',
32
+ 'cdn-cache-control': 'no-store',
33
+ 'cloudflare-cdn-cache-control': 'no-store',
34
+ }
35
+ }
36
+
37
+ if (path === indexPath || fileName === 'manifest.webmanifest') {
38
+ return {
39
+ 'cache-control': 'no-cache',
40
+ 'cdn-cache-control': 'no-cache',
41
+ 'cloudflare-cdn-cache-control': 'no-cache',
42
+ }
43
+ }
44
+
45
+ return {
46
+ 'cache-control': 'public, max-age=31536000, immutable',
47
+ 'cdn-cache-control': 'public, max-age=31536000, immutable',
48
+ 'cloudflare-cdn-cache-control': 'public, max-age=31536000, immutable',
49
+ }
50
+ }
51
+
52
+ function setForwardHeaders(headers, req) {
53
+ headers.set('x-forwarded-for', req.socket.remoteAddress ?? '')
54
+ headers.set('x-forwarded-host', req.headers.host ?? '')
55
+ headers.set('x-forwarded-proto', 'https')
56
+ }
57
+
58
+ async function proxyRequest(req, res) {
59
+ const targetURL = new URL(req.url ?? '/', apiOrigin)
60
+ const headers = new Headers()
61
+
62
+ for (const [key, value] of Object.entries(req.headers)) {
63
+ if (value === undefined) {
64
+ continue
65
+ }
66
+ if (Array.isArray(value)) {
67
+ for (const item of value) {
68
+ headers.append(key, item)
69
+ }
70
+ continue
71
+ }
72
+ headers.set(key, value)
73
+ }
74
+
75
+ setForwardHeaders(headers, req)
76
+
77
+ const init = {
78
+ method: req.method,
79
+ headers,
80
+ body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req,
81
+ duplex: 'half',
82
+ }
83
+
84
+ const upstream = await fetch(targetURL, init)
85
+
86
+ res.writeHead(
87
+ upstream.status,
88
+ Object.fromEntries(upstream.headers.entries()),
89
+ )
90
+
91
+ if (!upstream.body) {
92
+ res.end()
93
+ return
94
+ }
95
+
96
+ for await (const chunk of upstream.body) {
97
+ res.write(chunk)
98
+ }
99
+ res.end()
100
+ }
101
+
102
+ function resolveStaticPath(requestPath) {
103
+ const decodedPath = decodeURIComponent(requestPath.split('?')[0])
104
+ const normalizedPath = normalize(decodedPath).replace(/^(\.\.[/\\])+/, '')
105
+ const trimmedPath = normalizedPath.replace(/^[/\\]+/, '')
106
+ return join(distDir, trimmedPath)
107
+ }
108
+
109
+ async function fileExists(path) {
110
+ try {
111
+ await access(path)
112
+ return true
113
+ } catch {
114
+ return false
115
+ }
116
+ }
117
+
118
+ async function serveFile(res, path) {
119
+ const fileStats = await stat(path)
120
+ if (!fileStats.isFile()) {
121
+ return false
122
+ }
123
+
124
+ const type = contentTypes.get(extname(path)) ?? 'application/octet-stream'
125
+ res.writeHead(200, {
126
+ ...resolveCacheHeaders(path),
127
+ 'content-length': String(fileStats.size),
128
+ 'content-type': type,
129
+ })
130
+ createReadStream(path).pipe(res)
131
+ return true
132
+ }
133
+
134
+ async function serveApp(req, res) {
135
+ const requestPath = req.url ?? '/'
136
+ let candidatePath = resolveStaticPath(requestPath)
137
+
138
+ if (await fileExists(candidatePath)) {
139
+ const served = await serveFile(res, candidatePath)
140
+ if (served) {
141
+ return
142
+ }
143
+ }
144
+
145
+ candidatePath = indexPath
146
+ res.writeHead(200, {
147
+ ...resolveCacheHeaders(candidatePath),
148
+ 'content-type': 'text/html; charset=utf-8',
149
+ })
150
+ res.end(await readFile(candidatePath))
151
+ }
152
+
153
+ const server = http.createServer(async (req, res) => {
154
+ try {
155
+ const requestPath = req.url ?? '/'
156
+ if (requestPath.startsWith('/api/') || requestPath === '/api' || requestPath.startsWith('/static/uploads/')) {
157
+ await proxyRequest(req, res)
158
+ return
159
+ }
160
+
161
+ await serveApp(req, res)
162
+ } catch (error) {
163
+ console.error('gateway request failed', error)
164
+ if (!res.headersSent) {
165
+ res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
166
+ }
167
+ res.end('Bad gateway')
168
+ }
169
+ })
170
+
171
+ server.listen(port, host, () => {
172
+ console.log(`agent gateway listening on http://${host}:${port}`)
173
+ console.log(`serving dist from ${distDir}`)
174
+ console.log(`proxying API to ${apiOrigin}`)
175
+ })