2026-07-02 23:36:45 -07:00
package bootstrap
import (
"database/sql"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"time"
"github.com/italypaleale/francis/builtin/ratelimit"
"github.com/italypaleale/francis/components/postgres"
"github.com/italypaleale/francis/host/local"
"github.com/jackc/pgx/v5/pgxpool"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/job"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/storage"
"github.com/pocket-id/pocket-id/backend/internal/utils/crypto"
)
type NewActorsOpts struct {
SQLite * sql . DB
Postgres * pgxpool . Pool
EnvConfig * common . EnvConfigSchema
2026-07-11 07:16:05 -07:00
InstanceID string
2026-07-02 23:36:45 -07:00
HttpClient * http . Client
DB * gorm . DB
FileStorage storage . FileStorage
}
func NewActors ( o NewActorsOpts ) ( * local . Host , map [ string ] * ratelimit . RateLimitService , error ) {
log := slog . Default ( )
// Derive a PSK from the global encryption key
// The runtime PSK derives the cluster CA used for host-to-host mTLS
psk , err := o . getPSK ( )
if err != nil {
return nil , nil , fmt . Errorf ( "failed to derive PSK: %w" , err )
}
// Options for the host
opts := [ ] local . HostOption {
local . WithAddress ( net . JoinHostPort ( o . EnvConfig . ActorsHost , o . EnvConfig . ActorsPort ) ) ,
local . WithLogger ( log . With ( "scope" , "actor-host" ) ) ,
local . WithRuntimePSKs ( psk ) ,
local . WithShutdownGracePeriod ( 10 * time . Second ) ,
}
2026-07-11 07:16:05 -07:00
// Add the database connection
providerOpt , err := o . getProvider ( )
2026-07-02 23:36:45 -07:00
if err != nil {
return nil , nil , err
}
2026-07-11 07:16:05 -07:00
opts = append ( opts , providerOpt )
2026-07-02 23:36:45 -07:00
2026-07-11 07:16:05 -07:00
// Create a new actor host
h , err := local . NewHost ( opts ... )
2026-07-02 23:36:45 -07:00
if err != nil {
2026-07-11 07:16:05 -07:00
return nil , nil , fmt . Errorf ( "failed to create actor host: %w" , err )
2026-07-02 23:36:45 -07:00
}
2026-07-11 07:16:05 -07:00
// Add all cron jobs
err = o . registerCronJobs ( h )
2026-07-02 23:36:45 -07:00
if err != nil {
return nil , nil , err
}
2026-07-11 07:16:05 -07:00
// Add the rate limiters
rateLimiters , err := o . registerRateLimiters ( h )
2026-07-02 23:36:45 -07:00
if err != nil {
2026-07-11 07:16:05 -07:00
return nil , nil , err
2026-07-02 23:36:45 -07:00
}
// Bind a service for each rate limiter so the middleware can invoke them
rateLimitServices := make ( map [ string ] * ratelimit . RateLimitService , len ( rateLimiters ) )
for name , rl := range rateLimiters {
rateLimitServices [ name ] = rl . Service ( h . Service ( ) )
}
return h , rateLimitServices , nil
}
// Derive a PSK from the global encryption key
func ( o * NewActorsOpts ) getPSK ( ) ( [ ] byte , error ) {
2026-07-11 07:16:05 -07:00
// This is tied to the instance ID of the Pocket ID deployment/cluster
2026-07-02 23:36:45 -07:00
// Note: changing the key derivation or the seed is a breaking change
2026-07-11 07:16:05 -07:00
return crypto . DeriveKey ( o . EnvConfig . EncryptionKey , "pocketid/actors-psk/" + o . InstanceID )
2026-07-02 23:36:45 -07:00
}
func ( o * NewActorsOpts ) getProvider ( ) ( local . HostOption , error ) {
switch {
case o . Postgres != nil && o . SQLite != nil :
return nil , errors . New ( "cannot have both Postgres and SQLite connections" )
case o . Postgres != nil :
return local . WithPostgresProvider ( postgres . PostgresProviderOptions {
DB : o . Postgres ,
} ) , nil
case o . SQLite != nil :
return local . WithSQLiteProvider ( local . SQLiteProviderOptions {
DB : o . SQLite ,
} ) , nil
default :
return nil , errors . New ( "one of Postgres and SQLite must be set" )
}
}
2026-07-11 07:16:05 -07:00
func ( o * NewActorsOpts ) registerCronJobs ( host * local . Host ) ( err error ) {
2026-07-02 23:36:45 -07:00
// In test mode, we do not register anything
if common . EnvConfig . AppEnv == "test" {
2026-07-11 07:16:05 -07:00
return nil
2026-07-02 23:36:45 -07:00
}
// Register the analytics job
2026-07-11 07:16:05 -07:00
analyticsJob , err := job . GetAnalyticsJob ( o . HttpClient , o . InstanceID )
2026-07-02 23:36:45 -07:00
if err != nil {
2026-07-11 07:16:05 -07:00
return fmt . Errorf ( "failed to get analytics cron job: %w" , err )
2026-07-02 23:36:45 -07:00
}
2026-07-11 07:16:05 -07:00
// This could be nil if analytics are disabled
2026-07-02 23:36:45 -07:00
if analyticsJob != nil {
2026-07-11 07:16:05 -07:00
err = host . RegisterBuiltInActor ( analyticsJob )
if err != nil {
return fmt . Errorf ( "error registering built-in actor for analytics job: %w" , err )
}
2026-07-02 23:36:45 -07:00
}
// Register the file cleanup jobs
fileCleanupJobs , err := job . GetFileCleanupJobs ( o . DB , o . FileStorage )
if err != nil {
2026-07-11 07:16:05 -07:00
return fmt . Errorf ( "failed to get file cleanup cron jobs: %w" , err )
2026-07-02 23:36:45 -07:00
}
for _ , j := range fileCleanupJobs {
2026-07-11 07:16:05 -07:00
err = host . RegisterBuiltInActor ( j )
if err != nil {
return fmt . Errorf ( "error registering built-in actor for cleanup job: %w" , err )
}
2026-07-02 23:36:45 -07:00
}
2026-07-11 07:16:05 -07:00
return nil
2026-07-02 23:36:45 -07:00
}
2026-07-11 07:16:05 -07:00
// registerRateLimiters creates a built-in rate-limit actor for each middleware policy and returns both the created actors (keyed by policy name) and the host options to register them
2026-07-02 23:36:45 -07:00
// Unlike cron jobs, rate limiters keep no durable state, so they are registered in every environment
2026-07-11 07:16:05 -07:00
func ( o * NewActorsOpts ) registerRateLimiters ( host * local . Host ) ( actors map [ string ] * ratelimit . RateLimit , err error ) {
2026-07-02 23:36:45 -07:00
policies := middleware . RateLimitPolicies ( )
actors = make ( map [ string ] * ratelimit . RateLimit , len ( policies ) )
for _ , p := range policies {
rl , err := ratelimit . New (
p . Name ,
ratelimit . WithRate ( p . Rate ) ,
ratelimit . WithPer ( p . Per ) ,
ratelimit . WithBurst ( p . Burst ) ,
)
if err != nil {
2026-07-11 07:16:05 -07:00
return nil , fmt . Errorf ( "error creating rate limiter %q: %w" , p . Name , err )
2026-07-02 23:36:45 -07:00
}
actors [ p . Name ] = rl
2026-07-11 07:16:05 -07:00
err = host . RegisterBuiltInActor ( rl )
if err != nil {
return nil , fmt . Errorf ( "error registering built-in actor for rate limiter '%s': %w" , p . Name , err )
}
2026-07-02 23:36:45 -07:00
}
2026-07-11 07:16:05 -07:00
return actors , nil
2026-07-02 23:36:45 -07:00
}