{"contents":"package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"tangled.org/core/api/tangled\"\n\t\"tangled.org/core/idresolver\"\n\t\"tangled.org/core/jetstream\"\n\t\"tangled.org/core/notifier\"\n\t\"tangled.org/core/rbac\"\n\t\"tangled.org/core/xrpc/serviceauth\"\n\n\t\"tangled.org/http-knot/config\"\n\t\"tangled.org/http-knot/db\"\n\t\"tangled.org/http-knot/events\"\n\t\"tangled.org/http-knot/githttp\"\n\t\"tangled.org/http-knot/ingester\"\n\txrpcpkg \"tangled.org/http-knot/xrpc\"\n)\n\nfunc main() {\n\tlogger := slog.New(slog.NewTextHandler(os.Stderr, \u0026slog.HandlerOptions{Level: slog.LevelInfo}))\n\n\tctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)\n\tdefer cancel()\n\n\tif err := run(ctx, logger); err != nil {\n\t\tlogger.Error(\"fatal\", \"err\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc run(ctx context.Context, logger *slog.Logger) error {\n\tcfg, err := config.Load(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"load config: %w\", err)\n\t}\n\n\t// Database\n\tdatabase, err := db.Setup(cfg.Server.DBPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setup db: %w\", err)\n\t}\n\n\t// RBAC\n\tenforcer, err := rbac.NewEnforcer(cfg.Server.DBPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setup rbac: %w\", err)\n\t}\n\tif err := enforcer.AddKnot(rbac.ThisServer); err != nil {\n\t\treturn fmt.Errorf(\"setup knot rbac: %w\", err)\n\t}\n\n\t// Configure owner\n\tif err := configureOwner(cfg, database, enforcer); err != nil {\n\t\treturn fmt.Errorf(\"configure owner: %w\", err)\n\t}\n\tlogger.Info(\"owner set\", \"did\", cfg.Server.Owner)\n\n\t// Notifier\n\tn := notifier.New()\n\n\t// ID Resolver\n\tresolver := idresolver.DefaultResolver(cfg.Server.PlcUrl)\n\n\t// Service Auth\n\tsa := serviceauth.NewServiceAuth(logger, resolver, cfg.Server.Did())\n\n\t// Jetstream\n\tcollections := []string{\n\t\ttangled.KnotMemberNSID,\n\t\ttangled.RepoCollaboratorNSID,\n\t}\n\tjc, err := jetstream.NewJetstreamClient(\n\t\tcfg.Server.JetstreamEndpoint,\n\t\tcfg.Server.Hostname,\n\t\tcollections,\n\t\tnil,\n\t\tlogger,\n\t\tdatabase,\n\t\tdatabase.HasKnownDids(),\n\t\ttrue,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setup jetstream: %w\", err)\n\t}\n\n\t// Load known DIDs into jetstream filter\n\tjc.AddDid(cfg.Server.Owner)\n\tdids, err := database.GetAllDids()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get known dids: %w\", err)\n\t}\n\tfor _, d := range dids {\n\t\tjc.AddDid(d)\n\t}\n\n\t// Ingester\n\ting := ingester.New(database, enforcer, cfg.Server.Hostname, logger)\n\tif err := jc.StartJetstream(ctx, ing.ProcessMessages); err != nil {\n\t\treturn fmt.Errorf(\"start jetstream: %w\", err)\n\t}\n\n\t// Handlers\n\tgitHandler := githttp.NewHandler(cfg, database, enforcer, \u0026n, logger)\n\teventsHandler := events.NewHandler(database, \u0026n, logger)\n\n\t// Router\n\tr := chi.NewRouter()\n\n\tr.Get(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"This is an http-knot server. More info at https://tangled.sh\"))\n\t})\n\n\tr.Route(\"/{did}\", func(r chi.Router) {\n\t\tr.Route(\"/{name}\", func(r chi.Router) {\n\t\t\t// Convert git's Basic Auth (password=JWT) to Bearer for all git routes\n\t\t\tr.Use(githttp.BasicToBearer())\n\n\t\t\t// InfoRefs: no auth for clone/fetch, auth required for push discovery\n\t\t\tr.Get(\"/info/refs\", githttp.ConditionalReceiveAuth(sa, logger)(gitHandler.InfoRefs))\n\n\t\t\t// Clone/fetch (no auth required)\n\t\t\tr.Post(\"/git-upload-pack\", gitHandler.UploadPack)\n\n\t\t\t// Push (auth required)\n\t\t\tr.Group(func(r chi.Router) {\n\t\t\t\tr.Use(githttp.RequireAuth(sa, logger))\n\t\t\t\tr.Use(githttp.RequirePush(enforcer, logger))\n\t\t\t\tr.Post(\"/git-receive-pack\", gitHandler.ReceivePack)\n\t\t\t})\n\t\t})\n\t})\n\n\t// XRPC endpoints\n\txrpcHandler := \u0026xrpcpkg.Xrpc{\n\t\tConfig: cfg,\n\t\tDB: database,\n\t\tEnforcer: enforcer,\n\t\tLogger: logger,\n\t\tNotifier: \u0026n,\n\t\tResolver: resolver,\n\t\tServiceAuth: sa,\n\t}\n\tr.Mount(\"/xrpc\", xrpcHandler.Router())\n\n\t// WebSocket events\n\tr.Get(\"/events\", eventsHandler.Events)\n\n\t// Start server\n\tsrv := \u0026http.Server{\n\t\tAddr: cfg.Server.ListenAddr,\n\t\tHandler: r,\n\t}\n\n\tgo func() {\n\t\t\u003c-ctx.Done()\n\t\tlogger.Info(\"shutting down\")\n\t\tsrv.Shutdown(context.Background())\n\t}()\n\n\tlogger.Info(\"starting server\", \"addr\", cfg.Server.ListenAddr, \"hostname\", cfg.Server.Hostname)\n\tif err := srv.ListenAndServe(); err != http.ErrServerClosed {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc configureOwner(cfg *config.Config, database *db.DB, enforcer *rbac.Enforcer) error {\n\towner := cfg.Server.Owner\n\n\texisting, err := enforcer.GetKnotUsersByRole(\"server:owner\", rbac.ThisServer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch len(existing) {\n\tcase 0:\n\t\t// No owner yet\n\tcase 1:\n\t\tif existing[0] == owner {\n\t\t\treturn nil\n\t\t}\n\t\t// Owner changed — remove old\n\t\tdatabase.RemoveDid(existing[0])\n\t\tenforcer.RemoveKnotOwner(rbac.ThisServer, existing[0])\n\tdefault:\n\t\treturn fmt.Errorf(\"multiple owners in RBAC, clean up DB at %s\", cfg.Server.DBPath)\n\t}\n\n\tif err := database.AddDid(owner); err != nil {\n\t\treturn fmt.Errorf(\"add owner to db: %w\", err)\n\t}\n\tif err := enforcer.AddKnotOwner(rbac.ThisServer, owner); err != nil {\n\t\treturn err\n\t}\n\treturn enforcer.E.SavePolicy()\n}\n","path":"main.go","ref":"main"}