Files
ozan a81a450e7e feat: monorepo consolidation — merge CLI, bot, admin, team-tool, website, docs, runner, proxy
Merged into tinqs/studio:
- cmd/tinqs-cli/    — tinqs-cli (Go binary, from bot/cli)
- cmd/tea/          — Gitea CLI tool (from tinqs/cli-tea)
- services/bot/     — Bot service (from tinqs-ltd/bot on git.arikigame.com)
- services/admin/   — Admin panel (from tinqs/admin)
- services/team-tool/ — Team Tool (from tinqs/team-tool)
- services/proxy/   — tinqs-proxy (from bot/proxy)
- web/landing/      — tinqs.com website (from tinqs/website)
- web/docs/         — Platform docs (from tinqs/docs)
- web/blog/         — Blog (placeholder)
- runner/           — Ephemeral CI runner (from tinqs/runner)

All source repos will be deleted after verification.
2026-05-22 04:55:50 +00:00

166 lines
4.6 KiB
Go

package main
import (
"crypto/tls"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
"golang.org/x/crypto/acme/autocert"
)
var version = "1.2.0"
// Routes: hostname → localhost:port.
// Compiled in — these change maybe once a quarter.
var routes = map[string]string{
"git.arikigame.com": "http://127.0.0.1:4100", // Gitea
"bot.arikigame.com": "http://127.0.0.1:5500", // bot platform (Tailscale IP via /etc/hosts + auth cookie)
"taco.arikigame.com": "http://127.0.0.1:5500", // meeting assistant (same process as bot)
"admin.arikigame.com": "http://127.0.0.1:7700", // admin portal
"langfuse.arikigame.com": "http://127.0.0.1:3100", // Langfuse LLM observability
"staging.tinqs.com": "http://127.0.0.1:3115", // Tinqs Platform (staging) — pm2 platform-staging
"platform.tinqs.com": "http://127.0.0.1:3120", // Tinqs Platform (production) — pm2 platform
}
// Tailscale-only hosts — unreachable from public internet. Defense-in-depth.
var tailscaleOnly = map[string]bool{
"admin.arikigame.com": true,
"langfuse.arikigame.com": true,
}
// tailscaleCIDR is 100.64.0.0/10 (CGNAT range Tailscale uses).
var tailscaleCIDR *net.IPNet
func init() {
_, tailscaleCIDR, _ = net.ParseCIDR("100.64.0.0/10")
}
func isTailscaleIP(remoteAddr string) bool {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
host = remoteAddr
}
ip := net.ParseIP(host)
return ip != nil && tailscaleCIDR.Contains(ip)
}
func realIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
func main() {
proxies := make(map[string]*httputil.ReverseProxy, len(routes))
domains := make([]string, 0, len(routes))
for host, target := range routes {
u, err := url.Parse(target)
if err != nil {
log.Fatalf("bad target for %s: %v", host, err)
}
rp := &httputil.ReverseProxy{
Director: func(h string, u *url.URL) func(req *http.Request) {
return func(req *http.Request) {
req.URL.Scheme = u.Scheme
req.URL.Host = u.Host
req.Host = h
ip := realIP(req)
req.Header.Set("X-Real-IP", ip)
req.Header.Set("X-Forwarded-For", ip)
req.Header.Set("X-Forwarded-Proto", "https")
}
}(host, u),
ErrorLog: log.New(os.Stderr, "["+host+"] ", log.LstdFlags),
// Flush immediately for SSE / streaming responses
FlushInterval: -1,
}
proxies[host] = rp
domains = append(domains, host)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := strings.ToLower(strings.Split(r.Host, ":")[0])
proxy, ok := proxies[host]
if !ok {
http.Error(w, "unknown host", http.StatusNotFound)
return
}
if tailscaleOnly[host] && !isTailscaleIP(r.RemoteAddr) {
http.Error(w, "Tailscale required — install at https://tailscale.com/download and sign in with @tinqs.com", http.StatusForbidden)
return
}
proxy.ServeHTTP(w, r)
})
// Certificate storage
certDir := "/var/lib/tinqs-proxy/certs"
if d := os.Getenv("CERT_DIR"); d != "" {
certDir = d
}
mgr := &autocert.Manager{
Cache: autocert.DirCache(certDir),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(domains...),
}
// :80 — ACME HTTP-01 challenges, Tailscale-only hosts served directly, rest redirect to HTTPS
go func() {
httpSrv := &http.Server{
Addr: ":80",
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
Handler: mgr.HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := strings.Split(r.Host, ":")[0]
// Tailscale-only hosts: serve on HTTP directly (WireGuard is already encrypted)
if tailscaleOnly[host] {
proxy, ok := proxies[host]
if ok && isTailscaleIP(r.RemoteAddr) {
proxy.ServeHTTP(w, r)
return
}
http.Error(w, "Tailscale required", http.StatusForbidden)
return
}
target := "https://" + r.Host + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusMovedPermanently)
})),
}
log.Printf("tinqs-proxy :80 (ACME + redirect)")
if err := httpSrv.ListenAndServe(); err != nil {
log.Fatalf(":80 failed: %v", err)
}
}()
// :443 — TLS with autocert
srv := &http.Server{
Addr: ":443",
Handler: handler,
TLSConfig: &tls.Config{
GetCertificate: mgr.GetCertificate,
MinVersion: tls.VersionTLS12,
},
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Minute, // 37 GB repos need time
IdleTimeout: 5 * time.Minute,
}
log.Printf("tinqs-proxy v%s starting on :443", version)
for _, d := range domains {
log.Printf(" %s → %s", d, routes[d])
}
log.Fatal(srv.ListenAndServeTLS("", ""))
}