labeler

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Overview

Package labeler implements the ATCR labeler service, an ATProto-compatible content moderation service for issuing takedown labels on container registry content.

Index

Constants

LabelVersion is the ATProto label format version (currently 1).

Variables

This section is empty.

Functions

func CreateLabel

func CreateLabel(db *sql.DB, l *Label) (int64, error)

CreateLabel inserts a freshly signed label and returns its sequence id. Caller must Sign() first — CreateLabel rejects rows missing a signature.

func CreateTakedown

func CreateTakedown(db *sql.DB, t *Takedown) (int64, error)

CreateTakedown inserts a takedown event row and returns its id. The id should then be stamped onto every label produced by this takedown (positive labels at issue time, negation labels at reversal time) so the audit trail stays linked.

func ExampleYAML

func ExampleYAML() ([]byte, error)

ExampleYAML generates an example labeler configuration file.

func LatestSeq

func LatestSeq(db *sql.DB) (int64, error)

LatestSeq returns the highest sequence id in the database, or 0 if empty.

func LoadIdentity

func LoadIdentity(ctx context.Context, cfg *Config) (string, *atcrypto.PrivateKeyK256, error)

LoadIdentity resolves the labeler's DID and loads its k256 signing key. For did:plc this calls into the shared PLC package (loading or creating); for did:web the DID is derived from PublicURL and the signing key is generated on disk if missing.

func MarkTakedownReversed

func MarkTakedownReversed(db *sql.DB, id int64, by string, at time.Time) error

MarkTakedownReversed sets the reversed_at / reversed_by fields on the takedown row. Refuses to overwrite an existing reversal.

Types

type Auth

type Auth struct {
	// contains filtered or unexported fields
}

Auth manages in-memory admin sessions for the labeler.

func NewAuth

func NewAuth(ownerDID string) *Auth

NewAuth wires a fresh in-memory session store keyed to the configured owner DID.

func (*Auth) CreateSession

func (a *Auth) CreateSession(did, handle, userAgent, ipPrefix string) (string, *Session, error)

CreateSession installs a new in-memory session and returns its cookie token alongside the embedded CSRF token (for echoing into forms).

func (*Auth) DeleteSession

func (a *Auth) DeleteSession(token string)

DeleteSession removes a session by cookie token (logout).

func (*Auth) GetSession

func (a *Auth) GetSession(token string) *Session

GetSession returns the session for the cookie token, evicting expired entries on access.

func (*Auth) RequireCSRF

func (a *Auth) RequireCSRF(next http.Handler) http.Handler

RequireCSRF validates a per-session CSRF token on state-mutating requests. Safe methods pass through. Token comes from X-CSRF-Token header or the csrf_token form field for application/x-www-form-urlencoded bodies. Must run after RequireOwner.

func (*Auth) RequireOwner

func (a *Auth) RequireOwner(next http.Handler) http.Handler

RequireOwner enforces a valid session bound to the owner DID, with UA / IP-prefix replay defense. State-mutating methods then go through the CSRF check below.

type Config

type Config struct {
	Version    string                  `yaml:"version" comment:"Configuration format version."`
	LogLevel   string                  `yaml:"log_level" comment:"Log level: debug, info, warn, error."`
	Labeler    LabelerConfig           `yaml:"labeler" comment:"Labeler service settings."`
	LogShipper config.LogShipperConfig `yaml:"log_shipper" comment:"Remote log shipping settings."`
}

Config represents the labeler service configuration. It is fully self-contained: no fields are inherited from or shared with the appview config.

func LoadConfig

func LoadConfig(yamlPath string) (*Config, error)

LoadConfig loads the labeler configuration from a YAML file.

func (*Config) DBPath

func (c *Config) DBPath() string

DBPath returns the path to the SQLite database file inside the data dir.

func (*Config) PLCDirectoryURL

func (c *Config) PLCDirectoryURL() string

PLCDirectoryURL returns the configured PLC directory URL or the canonical default.

func (*Config) PublicURL

func (c *Config) PublicURL() string

PublicURL returns the labeler's externally reachable URL.

func (*Config) SigningKeyPath

func (c *Config) SigningKeyPath() string

SigningKeyPath returns the configured signing key path or the default inside DataDir.

type Hub

type Hub struct {
	// contains filtered or unexported fields
}

Hub broadcasts newly-inserted labels to all live subscribeLabels clients.

func NewHub

func NewHub() *Hub

NewHub returns an empty hub ready to accept subscribers.

func (*Hub) Broadcast

func (h *Hub) Broadcast(l *Label)

Broadcast sends a copy of the label to every live subscriber. Subscribers whose buffer is full are evicted on the spot rather than slowing down the writer.

func (*Hub) Len

func (h *Hub) Len() int

Len returns the number of live subscribers (mostly for tests / metrics).

type Label

type Label struct {
	ID          int64
	Src         string
	URI         string
	CID         string
	Val         string
	Neg         bool
	Cts         time.Time
	Exp         *time.Time
	Ver         int64
	Sig         []byte
	SubjectDID  string
	SubjectRepo string
	TakedownID  *int64
}

Label represents an ATProto label record stored locally. Its on-the-wire representation is produced by ToLabeling() which round-trips through indigo's labeling package so the signature stays valid byte-for-byte.

TakedownID is a labeler-internal pointer to the takedown event that produced this label (positive or negation). It's never serialized into ATProto wire format.

func GetLabelsByTakedown

func GetLabelsByTakedown(db *sql.DB, takedownID int64) ([]Label, error)

GetLabelsByTakedown returns all labels (positive + negations) linked to a takedown.

func GetLabelsSince

func GetLabelsSince(db *sql.DB, cursor int64, limit int) ([]Label, error)

GetLabelsSince returns labels with id > cursor, ordered by id ascending.

func NegateTakedownLabels

func NegateTakedownLabels(db *sql.DB, key *atcrypto.PrivateKeyK256, src string, takedownID int64) ([]Label, error)

NegateTakedownLabels signs+inserts negation labels for every active (non-negated) label linked to the given takedown_id. Negations carry the same takedown_id so they remain part of the takedown's audit trail.

The NOT EXISTS subquery skips URIs that already have a later neg=1 row (from a prior reversal call or from an external negation streamed in via subscribeLabels), so this function is idempotent and won't emit duplicate negations.

func (*Label) Sign

func (l *Label) Sign(key *atcrypto.PrivateKeyK256) error

Sign computes a k256 signature over the deterministic CBOR encoding of the label (without the sig field) and stores it on the row.

func (*Label) ToLabeling

func (l *Label) ToLabeling() labeling.Label

ToLabeling converts the row into indigo's label struct (deterministic CBOR shape).

type LabelerConfig

type LabelerConfig struct {
	// Enable the labeler service.
	Enabled bool `yaml:"enabled" comment:"Enable the labeler service."`

	// Listen address for the labeler HTTP server.
	Addr string `yaml:"addr" comment:"Listen address for labeler (e.g., :5002)."`

	// PublicURL is the externally reachable URL of the labeler. Required.
	PublicURL string `yaml:"public_url" comment:"Externally reachable labeler URL (required, e.g. https://labeler.example.com)."`

	// ClientName is the OAuth client display name shown to PDS users on consent screens.
	ClientName string `yaml:"client_name" comment:"OAuth client display name (e.g., \"ATCR Labeler\")."`

	// ClientShortName is a shorter brand label used in UI copy.
	ClientShortName string `yaml:"client_short_name" comment:"Short brand label used in UI copy (e.g., \"ATCR\")."`

	// DID of the labeler admin. Only this DID can log into the admin panel.
	OwnerDID string `yaml:"owner_did" comment:"DID of the labeler admin. Only this DID can log into the admin panel."`

	// Directory for labeler state: SQLite database, signing key, did.txt.
	DataDir string `yaml:"data_dir" comment:"Directory for labeler state (database, signing key, did.txt)."`

	// DID method: "plc" (recommended, portable) or "web" (hostname-bound).
	DIDMethod string `yaml:"did_method" comment:"DID method: \"plc\" (recommended) or \"web\"."`

	// Explicit DID for did:plc adoption/recovery (optional).
	DID string `yaml:"did" comment:"Explicit did:plc identifier for adoption/recovery (optional)."`

	// Signing key path (defaults to <DataDir>/signing.key).
	KeyPath string `yaml:"key_path" comment:"Path to K-256 signing key (defaults to <data_dir>/signing.key)."`

	// Rotation key multibase (K-256 or P-256). Required to update the PLC document.
	RotationKey string `yaml:"rotation_key" comment:"Multibase-encoded rotation key (K-256 or P-256). Required to update the PLC document."`

	// PLC directory URL (default https://plc.directory).
	PLCDirectoryURL string `yaml:"plc_directory_url" comment:"PLC directory URL (default https://plc.directory)."`

	// LibsqlSyncURL enables embedded-replica sync to a remote libSQL/Bunny database when set.
	// Empty = local-only mode (default).
	LibsqlSyncURL string `yaml:"libsql_sync_url" comment:"Optional libSQL/Bunny remote sync URL. Empty = local-only."`

	// LibsqlAuthToken is the auth token for the remote libSQL database.
	LibsqlAuthToken string `yaml:"libsql_auth_token" comment:"Auth token for libsql_sync_url."`

	// LibsqlSyncInterval is how often the embedded replica pulls from the remote.
	LibsqlSyncInterval time.Duration `yaml:"libsql_sync_interval" comment:"Embedded-replica pull interval (e.g. 30s). 0 = manual sync only."`
}

LabelerConfig defines labeler-specific settings.

type LabelerDB

type LabelerDB struct {
	DB *sql.DB
	// contains filtered or unexported fields
}

LabelerDB wraps the *sql.DB plus its libsql connector (when in embedded-replica mode) so the caller can release file locks on shutdown.

func OpenDB

func OpenDB(dbPath string, sync LibsqlSync) (*LabelerDB, error)

OpenDB opens or creates the labeler database. When sync.SyncURL is set, the DB runs in embedded-replica mode (writes go to the remote, frames replicate to the local file); otherwise it's a plain local libSQL file. Schema is applied either way.

func (*LabelerDB) Close

func (l *LabelerDB) Close() error

Close closes the database and the libsql connector (if any). The connector close is what releases file locks; without it a subsequent local-only open errors with "database is locked" — the same gotcha the hold ran into.

type LibsqlSync

type LibsqlSync struct {
	SyncURL      string
	AuthToken    string
	SyncInterval time.Duration
}

LibsqlSync configures optional embedded-replica sync to a remote libSQL database. SyncURL empty means local-only mode.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the labeler HTTP server.

func NewServer

func NewServer(cfg *Config) (*Server, error)

NewServer creates a new labeler server.

func (*Server) ExecuteTakedown

func (s *Server) ExecuteTakedown(ctx context.Context, input *TakedownInput) (*TakedownResult, error)

ExecuteTakedown creates a takedown event row and the labels that belong to it. Every label (the user-level label, the per-record labels discovered via PDS, and the repo-level summary label) carries the new takedown_id so reversal can target the exact set without re-querying by subject.

func (*Server) ReverseTakedown

func (s *Server) ReverseTakedown(ctx context.Context, takedownID int64, reversedBy string) (*TakedownResult, error)

ReverseTakedown negates every active label belonging to the given takedown event and marks the event row as reversed. Refuses to act on a takedown that doesn't exist or has already been reversed.

func (*Server) Serve

func (s *Server) Serve() error

Serve starts the HTTP server with graceful shutdown.

type Session

type Session struct {
	DID       string
	Handle    string
	CSRFToken string
	CreatedAt time.Time
	UserAgent string
	IPPrefix  string
}

Session represents an authenticated admin session. Restart wipes the in-memory map so any stolen cookie token becomes useless after a restart, by design.

UserAgent and IPPrefix are captured at login and rechecked on every request — a stolen token replayed from a different browser or network prefix is rejected and the session is torn down. Empty bound values (Unix sockets, tests, unusual proxies) opt out rather than locking users out. Binding at /24 (IPv4) / /64 (IPv6) tolerates DHCP renewals within a prefix without inviting cross-network replay.

func SessionFromContext

func SessionFromContext(ctx context.Context) *Session

SessionFromContext returns the session attached to the request context, if any.

type Takedown

type Takedown struct {
	ID            int64
	Input         string
	SubjectDID    string
	SubjectRepo   string
	SubjectHandle string
	Reason        string
	CreatedAt     time.Time
	CreatedBy     string
	ReversedAt    *time.Time
	ReversedBy    string
	LabelCount    int
}

Takedown is a single operator-issued takedown action. Each Takedown owns one or more Label rows linked by takedown_id. Reversal sets reversed_at / reversed_by in place.

func GetTakedown

func GetTakedown(db *sql.DB, id int64) (*Takedown, error)

GetTakedown loads a single takedown row by id. Returns sql.ErrNoRows when missing.

func ListTakedowns

func ListTakedowns(db *sql.DB, filter TakedownFilter, limit, offset int) ([]Takedown, int, error)

ListTakedowns returns takedown events ordered by created_at DESC, scoped by filter. The total count reflects the same filter.

type TakedownFilter

type TakedownFilter int

TakedownFilter scopes ListTakedowns to active, reversed, or all rows.

const (
	TakedownAll      TakedownFilter = iota // every takedown row, regardless of reversal state
	TakedownActive                         // only takedowns whose reversed_at is NULL
	TakedownReversed                       // only takedowns whose reversed_at is set
)

type TakedownInput

type TakedownInput struct {
	DID        string
	Handle     string
	Repository string // empty = user-level takedown
	// Operator-supplied context. Captured into the takedowns row so we can show
	// who/why/what-was-typed on the dashboard. None are required.
	RawInput  string // exact string the operator submitted (URL, did, handle, AT URI)
	Reason    string // optional free-text note
	CreatedBy string // operator DID from session, "" if unknown
}

TakedownInput represents parsed takedown input.

func ParseTakedownInput

func ParseTakedownInput(ctx context.Context, input string) (*TakedownInput, error)

ParseTakedownInput parses various input formats into a TakedownInput.

Supported shapes (dispatched in order):

  • at://<did-or-handle>[/collection/rkey] — ATProto AT URI
  • did:plc:..., did:web:... — bare DID, user-level takedown
  • URL with /u/<handle> or /r/<handle>/<repo> — appview routes (with or without scheme)
  • <handle> — bare handle, user-level takedown

Anything else is rejected. The appview's /r/ route uses the repo name as a single path segment so any trailing path (digest pages, tag tabs) is discarded; URL fragments and query strings are dropped in all cases.

type TakedownResult

type TakedownResult struct {
	TakedownID int64
	DID        string
	Handle     string
	Repository string
	Labels     []Label
	UserLevel  bool
}

TakedownResult contains the results of a takedown operation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL