Initial release: self-hostable APT repository server and CLI

urapt is a self-hostable APT repository server with a companion CLI for
pushing and managing Debian .deb packages.

Server (urapt-server):
- REST API + APT endpoint, SQLite storage (pure-Go modernc driver, no CGO)
- .deb files stored content-addressed on disk, reference-counted for dedup
- Server-managed RSA-4096 OpenPGP signing key (ProtonMail/go-crypto)
- APT indices (Release/InRelease/Packages[.gz/.xz]) generated on demand
  from the DB, cached in memory, signed with the server key
- Full APT model: repositories -> distributions -> components -> architectures
- Bearer-token auth for REST; HTTP Basic auth for private-repo APT reads
- First registrant becomes admin; repo-scoped permissions
  (read/write/read-write/admin) plus owner and server-admin roles
- Multipart package push with control-field extraction, list/show/delete,
  pool serving, blob ref-count cleanup
- Audit log

CLI (urapt):
- register/login/logout/whoami, token management
- repo/distro/component/arch CRUD, member management
- push/pull/ls/show/rm for packages
- apt-config helper that emits apt setup commands (key, sources.list,
  auth.conf for private repos)

Packaging & docs:
- Dockerfile (multi-stage distroless), docker-compose.yml, sample config
- README quick start, architecture overview, config reference, security notes
- PLAN.md design blueprint, CHANGELOG.md, GPL-3.0 LICENSE
- GitHub Actions CI (test, lint, cross-build for linux/darwin amd64/arm64)
- Makefile release target producing static binaries + tarballs + checksums

Tests cover the data-access layer, auth/permission checks, APT index
generation, .deb parsing, GPG signing, the REST API, and the typed API
client. Verified end-to-end on a Raspberry Pi (arm64) pushing and installing
a real package.
This commit is contained in:
2026-06-28 16:57:34 -05:00
commit 981587e83d
83 changed files with 12812 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
// Package api defines the request and response DTOs that form the urapt REST
// API contract. The server's restapi package produces these and the CLI's
// apiclient package consumes them; keeping them in one place prevents drift.
package api
import "urapt/shared/models"
// --- server / setup ---
// ServerInfo is the response from GET /server/info.
type ServerInfo struct {
Version string `json:"version"`
NeedsSetup bool `json:"needs_setup"`
DefaultKeyFingerprint string `json:"default_key_fingerprint"`
OpenRegistration bool `json:"open_registration"`
}
// --- auth ---
// RegisterRequest is the body for POST /auth/register.
type RegisterRequest struct {
Username string `json:"username" validate:"required,min=3,max=32,username"`
Password string `json:"password" validate:"required,min=8,max=256"`
}
// LoginRequest is the body for POST /auth/login.
type LoginRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
// AuthResponse is returned by register and login.
type AuthResponse struct {
User *models.User `json:"user"`
Token string `json:"token"`
}
// CreateTokenRequest is the body for POST /me/tokens.
type CreateTokenRequest struct {
Name string `json:"name" validate:"required,min=1,max=64"`
}
// ListResponse wraps a page of items.
type ListResponse[T any] struct {
Items []T `json:"items"`
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
}
// --- users (admin) ---
// UpdateUserRequest is the body for PATCH /users/:id.
type UpdateUserRequest struct {
IsAdmin *bool `json:"is_admin,omitempty"`
}
// --- repositories ---
// CreateRepoRequest is the body for POST /repositories.
type CreateRepoRequest struct {
Name string `json:"name"`
Visibility string `json:"visibility"`
Description string `json:"description,omitempty"`
}
// UpdateRepoRequest is the body for PATCH /repositories/:repo.
type UpdateRepoRequest struct {
Name *string `json:"name,omitempty"`
Visibility *string `json:"visibility,omitempty"`
Description *string `json:"description,omitempty"`
}
// AddMemberRequest is the body for POST /repositories/:repo/members.
type AddMemberRequest struct {
Username string `json:"username"`
Access string `json:"access"`
}
// UpdateMemberRequest is the body for PATCH /repositories/:repo/members/:username.
type UpdateMemberRequest struct {
Access string `json:"access"`
}
// --- structure ---
// CreateNamedRequest is the body for creating a distribution/component/arch.
type CreateNamedRequest struct {
Name string `json:"name"`
}