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
- func CreateLabel(db *sql.DB, l *Label) (int64, error)
- func CreateTakedown(db *sql.DB, t *Takedown) (int64, error)
- func ExampleYAML() ([]byte, error)
- func LatestSeq(db *sql.DB) (int64, error)
- func LoadIdentity(ctx context.Context, cfg *Config) (string, *atcrypto.PrivateKeyK256, error)
- func MarkTakedownReversed(db *sql.DB, id int64, by string, at time.Time) error
- type Auth
- func (a *Auth) CreateSession(did, handle, userAgent, ipPrefix string) (string, *Session, error)
- func (a *Auth) DeleteSession(token string)
- func (a *Auth) GetSession(token string) *Session
- func (a *Auth) RequireCSRF(next http.Handler) http.Handler
- func (a *Auth) RequireOwner(next http.Handler) http.Handler
- type Config
- type Hub
- type Label
- type LabelerConfig
- type LabelerDB
- type LibsqlSync
- type Server
- type Session
- type Takedown
- type TakedownFilter
- type TakedownInput
- type TakedownResult
Constants ¶
const LabelVersion int64 = labeling.ATPROTO_LABEL_VERSION
LabelVersion is the ATProto label format version (currently 1).
Variables ¶
This section is empty.
Functions ¶
func CreateLabel ¶
CreateLabel inserts a freshly signed label and returns its sequence id. Caller must Sign() first — CreateLabel rejects rows missing a signature.
func CreateTakedown ¶
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 ¶
ExampleYAML generates an example labeler configuration file.
func LoadIdentity ¶
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.
Types ¶
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth manages in-memory admin sessions for the labeler.
func (*Auth) CreateSession ¶
CreateSession installs a new in-memory session and returns its cookie token alongside the embedded CSRF token (for echoing into forms).
func (*Auth) DeleteSession ¶
DeleteSession removes a session by cookie token (logout).
func (*Auth) GetSession ¶
GetSession returns the session for the cookie token, evicting expired entries on access.
func (*Auth) RequireCSRF ¶
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.
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 ¶
LoadConfig loads the labeler configuration from a YAML file.
func (*Config) PLCDirectoryURL ¶
PLCDirectoryURL returns the configured PLC directory URL or the canonical default.
func (*Config) SigningKeyPath ¶
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.
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 ¶
GetLabelsByTakedown returns all labels (positive + negations) linked to a takedown.
func GetLabelsSince ¶
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 ¶
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 ¶
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.
type LibsqlSync ¶
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 (*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.
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 ¶
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 ¶
GetTakedown loads a single takedown row by id. Returns sql.ErrNoRows when missing.
func ListTakedowns ¶
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.