Files
warp/server/internal/storage/pocketbase/pocketbase.go
T
tajniak81andClaude Opus 5.5 74f7bc8805
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
keyserver: cache remote users' device and cross-signing keys
POST /keys/query for a remote user who shares a room with a local user is
now answered from a cache, fetched whole with GET /user/devices/{userId}
on a miss. Incoming m.device_list_update EDUs are applied when their
prev_id matches the cached stream_id; anything else marks the copy stale
and it is fetched again in the background, and a stale copy is never
served. m.signing_key_update replaces the cached cross-signing keys. A
user's copy is dropped once no local user shares a room with them, by a
keyserver listener on the roomserver's emit stage.

Migration 0009 adds keyserver_remote_users, keyserver_remote_device_keys
and keyserver_remote_cross_signing_keys for sqlite and postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:36:08 +02:00

608 lines
22 KiB
Go

// Package pocketbase is the placeholder for the PocketBase storage driver.
//
// The plan (PLAN.md section 4) is an appliance deployment: one binary that
// also brings admin auth, a filesystem abstraction for media (local or S3)
// and realtime for the admin panel. The event DAG is a poor fit for
// PocketBase's record model, so the roomserver and syncapi tables will use
// its underlying dbx connection directly rather than its collection API.
//
// Until that work starts (Phase 10) the driver is compiled in, registered
// and fails loudly, so that `database.driver: pocketbase` is a clear error
// instead of an unknown-driver one, and so the interface stays honest:
// every method the other drivers implement exists here too.
package pocketbase
import (
"context"
"fmt"
"log/slog"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
func init() {
storage.Register(storage.DriverPocketBase, Open)
}
// Database is the not-yet-implemented PocketBase driver.
type Database struct{}
// Open always fails with storage.ErrNotImplemented.
func Open(context.Context, storage.Config, *slog.Logger) (storage.Database, error) {
return nil, fmt.Errorf(
"pocketbase driver is not implemented yet (planned for Phase 10); use %q or %q: %w",
storage.DriverSQLite, storage.DriverPostgres, storage.ErrNotImplemented)
}
// Driver implements storage.Database.
func (*Database) Driver() string { return storage.DriverPocketBase }
// Ping implements storage.Database.
func (*Database) Ping(context.Context) error { return storage.ErrNotImplemented }
// Migrate implements storage.Database.
func (*Database) Migrate(context.Context) error { return storage.ErrNotImplemented }
// PendingMigrations implements storage.Database.
func (*Database) PendingMigrations(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
// Settings implements storage.Database.
func (*Database) Settings() storage.SettingsStore { return notImplementedSettings{} }
// Users implements storage.Database.
func (*Database) Users() storage.UserStore { return notImplementedUsers{} }
// Rooms implements storage.Database.
func (*Database) Rooms() storage.RoomStore { return notImplementedRooms{} }
// Sync implements storage.Database.
func (*Database) Sync() storage.SyncStore { return notImplementedSync{} }
// Federation implements storage.Database.
func (*Database) Federation() storage.FederationStore { return notImplementedFederation{} }
// Media implements storage.Database.
func (*Database) Media() storage.MediaStore { return notImplementedMedia{} }
// Keys implements storage.Database.
func (*Database) Keys() storage.KeyStore { return notImplementedKeys{} }
// Close implements storage.Database.
func (*Database) Close() error { return nil }
// notImplementedSettings satisfies storage.SettingsStore by refusing every
// call, so a partially wired-up PocketBase deployment cannot silently lose
// an admin's federation switch.
type notImplementedSettings struct{}
func (notImplementedSettings) Get(context.Context, string) (string, bool, error) {
return "", false, storage.ErrNotImplemented
}
func (notImplementedSettings) GetAll(context.Context) (map[string]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSettings) Set(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedSettings) Delete(context.Context, string) error {
return storage.ErrNotImplemented
}
// notImplementedUsers satisfies storage.UserStore the same way: a
// half-wired PocketBase deployment must not accept a registration it cannot
// store, or hand out an access token it cannot look up again.
type notImplementedUsers struct{}
func (notImplementedUsers) CreateAccount(context.Context, storage.NewAccount) (storage.Account, error) {
return storage.Account{}, storage.ErrNotImplemented
}
func (notImplementedUsers) Account(context.Context, string) (storage.Account, error) {
return storage.Account{}, storage.ErrNotImplemented
}
func (notImplementedUsers) PasswordHash(context.Context, string) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedUsers) SetPasswordHash(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetAdmin(context.Context, string, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetDisplayName(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetAvatarURL(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) ListAccounts(context.Context, storage.AccountFilter) ([]storage.Account, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedUsers) Stats(context.Context) (storage.Stats, error) {
return storage.Stats{}, storage.ErrNotImplemented
}
func (notImplementedUsers) CreateSession(context.Context, storage.NewSession) (storage.Device, error) {
return storage.Device{}, storage.ErrNotImplemented
}
func (notImplementedUsers) RefreshSession(context.Context, storage.RefreshedSession) (storage.TokenInfo, error) {
return storage.TokenInfo{}, storage.ErrNotImplemented
}
func (notImplementedUsers) TokenInfo(context.Context, string) (storage.TokenInfo, error) {
return storage.TokenInfo{}, storage.ErrNotImplemented
}
func (notImplementedUsers) TouchDevice(context.Context, string, string, string, string, int64) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) Device(context.Context, string, string) (storage.Device, error) {
return storage.Device{}, storage.ErrNotImplemented
}
func (notImplementedUsers) Devices(context.Context, string) ([]storage.Device, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedUsers) SetDeviceDisplayName(context.Context, string, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) DeleteDevices(context.Context, string, []string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedUsers) DeleteAllDevices(context.Context, string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedUsers) AccountData(context.Context, string, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedUsers) SetAccountData(context.Context, string, string, string, []byte) error {
return storage.ErrNotImplemented
}
// notImplementedRooms satisfies storage.RoomStore the same way. It matters
// more here than anywhere else: a room whose create event was accepted and
// not stored would hand a client a room ID that refers to nothing.
type notImplementedRooms struct{}
func (notImplementedRooms) CreateRoom(context.Context, storage.NewRoom) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) Room(context.Context, string) (storage.Room, error) {
return storage.Room{}, storage.ErrNotImplemented
}
func (notImplementedRooms) Write(context.Context, storage.RoomWrite) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedRooms) Event(context.Context, string, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) EventsByID(context.Context, string, []string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentState(context.Context, string) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentStateEvents(context.Context, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentStateEvent(context.Context, string, string, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) StateBefore(context.Context, string, string) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Extremities(context.Context, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Members(context.Context, string, []string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MembershipEntries(context.Context, string, []string) ([]storage.MembershipEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Membership(context.Context, string, string) (storage.MembershipEntry, error) {
return storage.MembershipEntry{}, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomsForUser(context.Context, string, []string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MembershipsForUser(context.Context, string, []string) ([]storage.SyncMembership, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) UserMembershipEvents(context.Context, string, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MaxStreamOrdering(context.Context) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedRooms) Timeline(context.Context, storage.TimelineRequest) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomsWithEvents(context.Context, []string, int64, int64) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) StateAtStream(context.Context, string, int64) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) TransactionEvent(context.Context, storage.TransactionRef) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedRooms) CreateAlias(context.Context, storage.RoomAlias) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) DeleteAlias(context.Context, string) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) Alias(context.Context, string) (storage.RoomAlias, error) {
return storage.RoomAlias{}, storage.ErrNotImplemented
}
func (notImplementedRooms) AliasesForRoom(context.Context, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) SetPublished(context.Context, string, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) PublishedRooms(context.Context, storage.RoomFilter) ([]storage.RoomSummary, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedRooms) ListRooms(context.Context, storage.RoomFilter) ([]storage.RoomSummary, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomSummary(context.Context, string) (storage.RoomSummary, error) {
return storage.RoomSummary{}, storage.ErrNotImplemented
}
func (notImplementedRooms) PurgeRoom(context.Context, string) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) RoomStats(context.Context) (storage.RoomStats, error) {
return storage.RoomStats{}, storage.ErrNotImplemented
}
func (notImplementedRooms) WriteOutliers(context.Context, string, []storage.StoredEvent) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) WriteBackfill(context.Context, string, []storage.BackfillEvent) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) InviteState(context.Context, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) HasJoinedMemberOn(context.Context, string, string) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedRooms) EventByID(context.Context, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) UsersSharingRooms(context.Context, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MemberChanges(context.Context, []string, int64, int64) ([]storage.MemberChange, error) {
return nil, storage.ErrNotImplemented
}
// notImplementedFederation satisfies storage.FederationStore the same way.
// A queue that accepted events and never stored them would lose them the
// moment federation was switched on.
type notImplementedFederation struct{}
func (notImplementedFederation) StoreServerKeys(context.Context, []storage.ServerKey) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) ServerKeys(context.Context, string, []string) ([]storage.ServerKey, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Enqueue(context.Context, []storage.QueuedItem) ([]storage.QueuedItem, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Queued(context.Context, string, string, int) ([]storage.QueuedItem, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Dequeue(context.Context, []int64) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) QueuedDestinations(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Destination(context.Context, string) (storage.Destination, error) {
return storage.Destination{}, storage.ErrNotImplemented
}
func (notImplementedFederation) SetDestination(context.Context, storage.Destination) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) Destinations(context.Context) ([]storage.DestinationSummary, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) InboundTransaction(context.Context, string, string) ([]byte, bool, error) {
return nil, false, storage.ErrNotImplemented
}
func (notImplementedFederation) RecordInboundTransaction(context.Context, string, string, []byte, int64) error {
return storage.ErrNotImplemented
}
// notImplementedSync satisfies storage.SyncStore the same way. A sync that
// returned an empty success would be worse than an error: a client would
// take it for "nothing has happened" and stop asking.
type notImplementedSync struct{}
func (notImplementedSync) StreamPositions(context.Context) (storage.StreamPositions, error) {
return storage.StreamPositions{}, storage.ErrNotImplemented
}
func (notImplementedSync) SetReceipt(context.Context, storage.Receipt) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedSync) Receipts(context.Context, []string, int64, int64) ([]storage.Receipt, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) ReceiptsForUser(context.Context, string, []string) ([]storage.Receipt, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) SendToDevice(context.Context, []storage.NewToDeviceMessage) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedSync) ToDeviceMessages(context.Context, string, string, int64, int64, int) ([]storage.ToDeviceMessage, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) DeleteToDeviceMessages(context.Context, string, string, int64) error {
return storage.ErrNotImplemented
}
func (notImplementedSync) SetPresence(context.Context, storage.PresenceState) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedSync) Presence(context.Context, string) (storage.PresenceState, error) {
return storage.PresenceState{}, storage.ErrNotImplemented
}
func (notImplementedSync) PresenceForUsers(context.Context, []string, int64, int64) ([]storage.PresenceState, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) AccountDataChanges(context.Context, string, int64, int64) ([]storage.AccountDataEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) AccountDataAt(context.Context, string, int64) ([]storage.AccountDataEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) SaveFilter(context.Context, string, []byte, int64) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedSync) Filter(context.Context, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
// notImplementedMedia satisfies storage.MediaStore the same way. An upload
// whose metadata was not stored would hand the client an mxc:// URI that
// refers to nothing.
type notImplementedMedia struct{}
func (notImplementedMedia) CreateMedia(context.Context, storage.Media) error {
return storage.ErrNotImplemented
}
func (notImplementedMedia) Media(context.Context, string, string) (storage.Media, error) {
return storage.Media{}, storage.ErrNotImplemented
}
func (notImplementedMedia) CompleteUpload(context.Context, string, string, storage.MediaContent) error {
return storage.ErrNotImplemented
}
func (notImplementedMedia) PendingUploads(context.Context, string, int64) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedMedia) DeleteExpiredUploads(context.Context, int64) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedMedia) Thumbnail(context.Context, string, string, int, int, string) (storage.Thumbnail, error) {
return storage.Thumbnail{}, storage.ErrNotImplemented
}
func (notImplementedMedia) StoreThumbnail(context.Context, storage.Thumbnail) error {
return storage.ErrNotImplemented
}
// Compile-time proof that the stub keeps up with the interfaces.
var (
_ storage.Database = (*Database)(nil)
_ storage.UserStore = notImplementedUsers{}
_ storage.RoomStore = notImplementedRooms{}
_ storage.SyncStore = notImplementedSync{}
_ storage.FederationStore = notImplementedFederation{}
_ storage.MediaStore = notImplementedMedia{}
_ storage.KeyStore = notImplementedKeys{}
)
// notImplementedKeys satisfies storage.KeyStore the same way. A key upload
// that reported success and kept nothing would leave every message sent to
// the device undecryptable.
type notImplementedKeys struct{}
func (notImplementedKeys) SetDeviceKeys(context.Context, storage.DeviceKeys) (storage.DeviceListUpdate, error) {
return storage.DeviceListUpdate{}, storage.ErrNotImplemented
}
func (notImplementedKeys) DeviceKeys(context.Context, string, []string) ([]storage.DeviceKeys, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteDeviceKeys(context.Context, string, []string) ([]storage.DeviceListUpdate, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) AddOneTimeKeys(context.Context, string, string, []storage.OneTimeKey) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) OneTimeKeyCounts(context.Context, string, string) (map[string]int, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) SetFallbackKeys(context.Context, string, string, []storage.OneTimeKey) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) UnusedFallbackKeyTypes(context.Context, string, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) ClaimKey(context.Context, string, string, string) (storage.ClaimedKey, bool, error) {
return storage.ClaimedKey{}, false, storage.ErrNotImplemented
}
func (notImplementedKeys) SetCrossSigningKeys(context.Context, string, map[string][]byte) (storage.DeviceListUpdate, error) {
return storage.DeviceListUpdate{}, storage.ErrNotImplemented
}
func (notImplementedKeys) CrossSigningKeys(context.Context, string) (map[string][]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) AddSignatures(context.Context, []storage.KeySignature) ([]storage.DeviceListUpdate, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) Signatures(context.Context, string) ([]storage.KeySignature, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) MarkDeviceListsChanged(context.Context, []string) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedKeys) DeviceListChanges(context.Context, []string, int64, int64) ([]storage.DeviceListChange, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeviceListPosition(context.Context, string) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedKeys) CreateBackup(context.Context, string, string, []byte) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedKeys) Backup(context.Context, string, string) (storage.BackupVersion, error) {
return storage.BackupVersion{}, storage.ErrNotImplemented
}
func (notImplementedKeys) UpdateBackup(context.Context, string, string, []byte) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteBackup(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) PutBackupKeys(context.Context, string, string, []storage.BackupKey) (storage.BackupVersion, error) {
return storage.BackupVersion{}, storage.ErrNotImplemented
}
func (notImplementedKeys) BackupKeys(context.Context, string, string, string, string) ([]storage.BackupKey, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteBackupKeys(context.Context, string, string, string, string) (storage.BackupVersion, error) {
return storage.BackupVersion{}, storage.ErrNotImplemented
}
func (notImplementedKeys) RemoteKeys(context.Context, string) (storage.RemoteKeys, bool, error) {
return storage.RemoteKeys{}, false, storage.ErrNotImplemented
}
func (notImplementedKeys) SetRemoteKeys(context.Context, storage.RemoteKeys) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedKeys) UpdateRemoteDevice(context.Context, storage.RemoteDeviceUpdate) (storage.RemoteUpdateResult, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedKeys) SetRemoteCrossSigningKeys(context.Context, string, map[string][]byte) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedKeys) RemoteKeyUsers(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteRemoteKeys(context.Context, []string) error {
return storage.ErrNotImplemented
}