Files
owen 981587e83d 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.
2026-06-28 16:57:34 -05:00

145 lines
4.9 KiB
SQL

-- 0001_init.sql: initial urapt schema.
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
username_lc TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE api_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
prefix TEXT NOT NULL,
token_hash TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT,
revoked_at TEXT
);
CREATE INDEX idx_api_tokens_user ON api_tokens(user_id);
CREATE INDEX idx_api_tokens_hash ON api_tokens(token_hash);
CREATE TABLE repositories (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
owner_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
visibility TEXT NOT NULL CHECK (visibility IN ('public','private')),
description TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE repository_members (
repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
access TEXT NOT NULL CHECK (access IN ('read','write','read-write','admin')),
created_at TEXT NOT NULL,
PRIMARY KEY (repository_id, user_id)
);
CREATE TABLE distributions (
id TEXT PRIMARY KEY,
repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE (repository_id, name)
);
CREATE TABLE components (
id TEXT PRIMARY KEY,
distribution_id TEXT NOT NULL REFERENCES distributions(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE (distribution_id, name)
);
CREATE TABLE architectures (
id TEXT PRIMARY KEY,
distribution_id TEXT NOT NULL REFERENCES distributions(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE (distribution_id, name)
);
CREATE TABLE packages (
id TEXT PRIMARY KEY,
repository_id TEXT NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT,
distribution_id TEXT NOT NULL REFERENCES distributions(id) ON DELETE CASCADE,
component_id TEXT NOT NULL REFERENCES components(id) ON DELETE RESTRICT,
name TEXT NOT NULL,
version TEXT NOT NULL,
architecture TEXT NOT NULL,
source TEXT,
maintainer TEXT,
priority TEXT,
section TEXT,
origin TEXT,
homepage TEXT,
description TEXT,
description_md5 TEXT,
depends TEXT,
pre_depends TEXT,
recommends TEXT,
suggests TEXT,
conflicts TEXT,
breaks TEXT,
provides TEXT,
replaces TEXT,
enhances TEXT,
installed_size INTEGER,
essential TEXT,
built_using TEXT,
tag TEXT,
raw_control TEXT NOT NULL,
filename TEXT NOT NULL,
pool_path TEXT NOT NULL,
size INTEGER NOT NULL,
md5sum TEXT NOT NULL,
sha1 TEXT NOT NULL,
sha256 TEXT NOT NULL,
uploaded_by_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TEXT NOT NULL,
UNIQUE (repository_id, distribution_id, component_id, name, version, architecture)
);
CREATE INDEX idx_packages_lookup ON packages(repository_id, distribution_id, component_id, name);
CREATE INDEX idx_packages_distro ON packages(repository_id, distribution_id);
CREATE INDEX idx_packages_arch ON packages(distribution_id, architecture);
CREATE INDEX idx_packages_sha256 ON packages(sha256);
CREATE TABLE blobs (
sha256 TEXT PRIMARY KEY,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
ref_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE gpg_keys (
id TEXT PRIMARY KEY,
fingerprint TEXT UNIQUE NOT NULL,
user_id TEXT NOT NULL,
public_key_armored TEXT NOT NULL,
private_key_armored TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
repository_id TEXT REFERENCES repositories(id) ON DELETE SET NULL,
action TEXT NOT NULL,
target TEXT,
details TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX idx_audit_repo ON audit_log(repository_id, created_at);
CREATE INDEX idx_audit_user ON audit_log(user_id, created_at);