Skip to content
quarry Artifact [e223b6d9ff]
Login

Artifact [e223b6d9ff]

Artifact e223b6d9ff4fa9289d2ea6d7803ae98015c3540e0d9c3c9c2cf1403a6798dd6d:


// Package oltp is quarry's forge OLTP database (0001 §8): the only
// authoritative non-DAG state. v1 holds sync users — per-repo login
// credentials for push gating. Small by construction; the litestream
// replica is the recovery story when deployment warrants it.
package oltp

import (
	"database/sql"
	"fmt"
	"strings"

	"github.com/ncruces/go-sqlite3/driver"
)

const schema = `
CREATE TABLE IF NOT EXISTS sync_user (
	repo   TEXT NOT NULL,
	user   TEXT NOT NULL,
	secret TEXT NOT NULL, -- RFC §6.3 shared secret (40 hex); plaintext never stored
	PRIMARY KEY (repo, user)
);`

type DB struct {
	db *sql.DB
}

// allMigrations is every widening statement, in the order they are
// applied. Named rather than assembled at the call site so a test can
// check the same list the open runs — a guard reading a second copy
// would go stale the moment someone adds a group.
func allMigrations() []string {
	var out []string
	out = append(out, repoMigrations...)
	out = append(out, mirrorMigrations...)
	out = append(out, accountMigrations...)
	out = append(out, webhookMigrations...)
	out = append(out, ciMigrations...)
	return out
}

// allSchemas is every CREATE block, for the same reason.
func allSchemas() []string {
	return []string{
		repoSchema, accountSchema, sessionSchema, ciSchema, mirrorSchema,
		webhookSchema, subsSchema, voteSchema, pollSchema, moderationSchema,
		praiseSchema, protectSchema, checksSchema, autoSchema, categorySchema,
		readSchema, capsSchema, mentionSchema,
	}
}

func Open(path string) (*DB, error) {
	db, err := driver.Open("file:" + path)
	if err != nil {
		return nil, err
	}
	if _, err := db.Exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;`); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: pragmas: %w", err)
	}
	if _, err := db.Exec(schema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: schema: %w", err)
	}
	if _, err := db.Exec(accountSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: account schema: %w", err)
	}
	if _, err := db.Exec(webhookSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: webhook schema: %w", err)
	}
	if _, err := db.Exec(praiseSchema); err != nil {
		return nil, fmt.Errorf("oltp: praise schema: %w", err)
	}
	if _, err := db.Exec(sessionSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: session schema: %w", err)
	}
	if _, err := db.Exec(ciSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: ci schema: %w", err)
	}
	if _, err := db.Exec(mirrorSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: mirror schema: %w", err)
	}
	if _, err := db.Exec(repoSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: repo schema: %w", err)
	}
	if _, err := db.Exec(extCISchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: external ci schema: %w", err)
	}
	if _, err := db.Exec(checksSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: checks schema: %w", err)
	}
	if _, err := db.Exec(autoSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: auto-accept schema: %w", err)
	}
	if _, err := db.Exec(protectSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: protect schema: %w", err)
	}
	if _, err := db.Exec(voteSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: vote schema: %w", err)
	}
	if _, err := db.Exec(categorySchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: category schema: %w", err)
	}
	if _, err := db.Exec(pollSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: poll schema: %w", err)
	}
	if _, err := db.Exec(moderationSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: moderation schema: %w", err)
	}
	if _, err := db.Exec(savedViewSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: saved view schema: %w", err)
	}
	if _, err := db.Exec(subsSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: subs schema: %w", err)
	}
	if _, err := db.Exec(readSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: read schema: %w", err)
	}
	if _, err := db.Exec(capsSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: caps schema: %w", err)
	}
	if _, err := db.Exec(mentionSchema); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: mention schema: %w", err)
	}
	for _, alter := range allMigrations() {
		if _, err := db.Exec(alter); err != nil && !strings.Contains(err.Error(), "duplicate column") {
			db.Close()
			return nil, fmt.Errorf("oltp: migrate: %w", err)
		}
	}
	out := &DB{db: db}
	// Legacy role rows become cap strings on every open — idempotent,
	// and explicit caps are never clobbered (RFC 0004 §5).
	if err := out.MigrateRolesToCaps(); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: roles→caps: %w", err)
	}
	if err := out.seedFleetDefault(); err != nil {
		db.Close()
		return nil, fmt.Errorf("oltp: fleet default: %w", err)
	}
	return out, nil
}

func (d *DB) Close() error { return d.db.Close() }

// SetSyncUser stores (replacing) one repo user's shared secret.
func (d *DB) SetSyncUser(repo, user, secret string) error {
	_, err := d.db.Exec(`INSERT OR REPLACE INTO sync_user (repo, user, secret)
		VALUES (?, ?, ?)`, repo, user, secret)
	return err
}

// Secret returns the stored shared secret for one repo user.
func (d *DB) Secret(repo, user string) (string, bool) {
	var s string
	err := d.db.QueryRow(`SELECT secret FROM sync_user WHERE repo = ? AND user = ?`,
		repo, user).Scan(&s)
	return s, err == nil
}

// Gated reports whether a repo has any sync users — the v1 policy
// switch: no users means anonymous push stays allowed (local use),
// one or more users means pushes must authenticate.
func (d *DB) Gated(repo string) bool {
	var n int
	if err := d.db.QueryRow(`SELECT count(*) FROM sync_user WHERE repo = ?`, repo).Scan(&n); err != nil {
		return false
	}
	return n > 0
}