Skip to main content

Store

Struct Store 

Source
pub struct Store { /* private fields */ }
Expand description

The SQLite database, and every query the server makes against it.

Implementations§

Source§

impl Store

Source

pub fn audited<T>( &self, write: impl FnOnce(&Transaction<'_>, &str) -> Result<Outcome<T>>, build_leaf: impl FnOnce(u64, &str, &T) -> Vec<u8>, ) -> Result<T>

Runs write inside one transaction and, only if it commits, appends the leaf build_leaf makes from the position that leaf will hold (its seq), its at, and write’s own result — then, and only then, the in-memory tree learns about it.

write is given the same at, so a row it stamps and the leaf that records it carry one time. Both are taken under the lock the whole transaction holds, which is what makes at rise with seq.

This and Store::audited_each are the only places a leaf is ever appended, so “every authenticated state change appends its leaf, in the same transaction as the change” is true by construction: nothing calls INSERT INTO audit_log any other way, and a write that never commits — because write returned Outcome::Refuse or an error — leaves neither the state change nor a leaf behind.

Source

pub fn audited_each<T>( &self, write: impl FnOnce(&Transaction<'_>, &str) -> Result<Outcome<T>>, build_leaves: impl FnOnce(u64, &str, &T) -> Vec<Vec<u8>>, ) -> Result<T>

Store::audited, for a write that records any number of changes at once: build_leaves returns one leaf per change, the first taking seq and each after it the next, all appended in write’s transaction. The sweep is the one user: every device it removes has a leaf, and they commit together with the removals.

Source

pub fn audit_append( &self, build_leaf: impl FnOnce(u64, &str) -> Vec<u8>, ) -> Result<u64>

Appends a leaf with no other state to change: a pull, the server’s own start. Still one transaction (of one statement), so it shares Store::audited’s all-or-nothing behaviour rather than being a special case.

Returns the leaf’s seq, read out of the same closure audited calls under its lock — not by asking the tree its size again afterwards, which another append could have moved on by then.

Source

pub fn audit_checkpoint(&self) -> (u64, Hash)

The tree’s size and root, for GET /v1/audit/checkpoint and the Recall-Audit-Checkpoint header every pull carries.

Source

pub fn audit_entries( &self, start: u64, end: u64, max_bytes: usize, ) -> Result<Vec<AuditEntry>>

Leaves start to end - 1, stopping early once they come to more than max_bytes together — though never before the first, however large, so a caller paging on from the last seq it got always moves. The caller (the route handler) is responsible for end <= tree_size and the 1,000-row page limit.

Source

pub fn audit_consistency( &self, first: u64, second: u64, ) -> Result<Result<Vec<Hash>, ConsistencyError>>

The RFC 9162 §2.1.4 proof that the tree at second extends the one at first, from the tree in memory: a few hundred hashes at most, and no read of the database, however long the log.

Source§

impl Store

Source

pub fn create_enrollment( &self, e: &NewEnrollment<'_>, max_pending: usize, max_per_address: usize, ) -> Result<Created>

Stores a pending enrolment, unless its code is in use by another one still waiting, max_pending are waiting already, or max_per_address from its address are.

Source

pub fn poll_enrollment( &self, enrollment_id: &str, now: OffsetDateTime, interval: Duration, ) -> Result<Poll>

Answers a machine’s poll, and records when it asked.

A poll sooner than interval after the last one is told to slow down. A second of slack keeps a client that sleeps exactly the interval from being told so because its previous request spent a moment in flight.

Source

pub fn approve_enrollment_audited( &self, user_code: &str, device_id: &str, scope: &str, now: &str, expected_fingerprint: Option<&str>, build_leaf: impl FnOnce(u64, &str, &Device) -> Vec<u8>, ) -> Result<Decision<Device>>

Approves the enrolment waiting with user_code, making it device device_id, and appends the approve leaf build_leaf makes in the same transaction. When expected_fingerprint is given, the code’s key must have exactly that fingerprint.

build_leaf is called only once the device is real, so it can carry the device’s own public key: the leaf that lets a signature of this device’s be checked after the row itself is gone. A refusal (NotFound, Expired, KeyMismatch, NameTaken, or AlreadyDecided) rolls back and appends nothing.

Source

pub fn deny_enrollment_audited( &self, user_code: &str, now: &str, build_leaf: impl FnOnce(u64, &str, &str) -> Vec<u8>, ) -> Result<Decision<String>>

Denies the enrolment waiting with user_code, answering with the name it asked for, and appends the deny leaf in the same transaction.

Source

pub fn pending_enrollment( &self, user_code: &str, now: &str, ) -> Result<Decision<Waiting>>

What the enrolment waiting with user_code asked for, judged the way approving it would be, so a lookup and the approval after it never disagree about whether the code is still good.

Source

pub fn enroll_device_audited( &self, d: &NewDevice<'_>, max_for_key: Option<u32>, build_leaf: impl FnOnce(u64, &str, &Device) -> Vec<u8>, ) -> Result<Inserted>

Stores a device an authkey enrolled, and the enroll leaf build_leaf makes for it, in one transaction — unless an unrevoked device has its name or, when max_for_key is given, its authkey already has that many unrevoked devices, both checked in the same transaction, which then appends nothing.

The one way a device comes to exist without an approval, so its leaf is what carries its public key into the log: without it, nothing the device later signs could be checked offline once it is swept.

Source

pub fn device(&self, id: &str) -> Result<Option<Device>>

One device, revoked or not.

Source

pub fn enrolled_worker(&self) -> Result<Option<Device>>

The newest unrevoked device with the worker scope. While there is one, a stale push is queued for it rather than merged inline.

Source

pub fn devices(&self) -> Result<Vec<Device>>

Every device, newest first.

Source

pub fn revoke_device_audited( &self, id: &str, now: &str, build_leaf: impl FnOnce(u64, &str, &Device) -> Vec<u8>, ) -> Result<Option<Device>>

Revokes a device, with a revoke leaf appended when this call is what revoked it. Revoking one already revoked keeps the first time, changes nothing and appends nothing — there is no new fact for a leaf to record. None when there is no such device.

Source

pub fn touch_device(&self, id: &str, now: &str) -> Result<()>

Records that a device was just seen. Not a change the audit log records: see the module docs of store/audit.rs.

Source

pub fn insert_authkey_audited( &self, k: &NewAuthkey<'_>, build_leaf: impl FnOnce(u64, &str, &Authkey) -> Vec<u8>, ) -> Result<Authkey>

Stores an authkey’s hash and details, with its authkey_create leaf appended in the same transaction.

Source

pub fn authkey_by_hash(&self, key_sha256: &str) -> Result<Option<Authkey>>

The authkey whose SHA-256 is key_sha256, in any state.

Source

pub fn authkeys(&self) -> Result<Vec<Authkey>>

Every authkey, newest first.

Source

pub fn revoke_authkey_audited( &self, id: &str, now: &str, devices: bool, build_leaf: impl FnOnce(u64, &str, &Authkey, &[String]) -> Vec<u8>, ) -> Result<Option<Authkey>>

Revokes an authkey, keeping the first time if it already was, and with devices every device it enrolled that is not revoked yet; without it they are untouched. None when there is no such key.

One authkey_revoke leaf is appended when this call changed anything — revoked the key, or any device — naming the devices it revoked (build_leaf’s last argument), rather than one leaf more per cascaded device. A call that changes nothing, such as the same revoke sent twice, appends nothing. Asking for the devices of a key revoked earlier on its own does revoke them, and is recorded.

Source

pub fn sweep_devices_audited( &self, idle_before: &str, expired_before: &str, ) -> Result<(usize, usize)>

Removes ephemeral devices last seen (or, never seen, created) before idle_before, and enrolments that expired before expired_before. Answers how many of each went.

One transaction, with a sweep leaf for each device it removes, the server as their actor: the devices chosen and the devices deleted are the same rows, read and deleted under one lock, so no leaf claims a device the sweep did not remove, and none it removed goes without one. Enrolments carry no leaf: an unclaimed pending code is not a device or an authkey, the two kinds of row the log records.

Source§

impl Store

Source

pub fn write_and_queue_merge_audited( &self, project_key: &str, file_path: &str, incoming: &MergeSide, job_id: &str, now: OffsetDateTime, build_leaf: impl FnOnce(u64, &str, &Queued) -> Vec<u8>, ) -> Result<(Queued, String)>

Stores incoming as the file’s content, last-write-wins, and queues a merge of it with whatever it displaced, in one transaction with the push’s leaf, which build_leaf makes knowing what was queued: the job’s stored side is read under the same lock as the write, so it is exactly the version this push replaced. Answers what was queued and the write’s updated_at, which is its leaf’s at, as every audited write’s is; incoming.updated_at is not used.

Source

pub fn expire_leases_audited( &self, now: OffsetDateTime, build_leaf: impl Fn(u64, &str, &JobChange<'_>) -> Vec<u8>, ) -> Result<Vec<Failure>>

Puts every job whose lease ran out back in the queue, or fails it when that was its last attempt. Answers each job it failed.

Each job whose lease ran out gets a job_result leaf, which build_leaf makes, in the same transaction: the attempt ended with no result, and the job waits for another or has failed. A call that finds no lease run out changes nothing and appends nothing.

Source

pub fn claim_job_audited( &self, kinds: &[String], lease_id: &str, lease: Duration, now: OffsetDateTime, build_leaf: impl FnOnce(u64, &str, &Job) -> Vec<u8>, ) -> Result<Option<Job>>

Leases the oldest queued job of one of kinds that may run now, for lease from now, under lease_id, and appends the job_claim leaf build_leaf makes in the same transaction. Finding nothing to lease changes nothing and appends nothing.

Source

pub fn settle_job_audited( &self, id: &str, result: &ResultRequest, worker: &str, follow_up_id: &str, now: OffsetDateTime, build_leaf: impl FnOnce(u64, &str, &JobChange<'_>) -> Vec<u8>, ) -> Result<Settlement>

Records a worker’s result for job id, with the job_result leaf build_leaf makes from what it changed, in one transaction.

A result counts only under the job’s current, unexpired lease. The same result posted again under the lease that settled it changes nothing, answers as the first did, and appends nothing.

A merge is applied as a compare-and-swap: only if the file still has the hash of the version the job stored, and then attributed to worker. If another push landed meanwhile, the merged content becomes the stored side of a follow-up job, follow_up_id, against the newer version, at most MAX_LINKS deep. An empty merge of two versions that were not empty is never applied: it is taken as an error, and retried.

Source

pub fn jobs(&self, state: Option<&str>, limit: usize) -> Result<Vec<JobSummary>>

Jobs, newest first, at most limit, in state when given.

Source

pub fn retry_job_audited( &self, id: &str, now: OffsetDateTime, build_leaf: impl FnOnce(u64, &str, &JobSummary) -> Vec<u8>, ) -> Result<Retried>

Queues a failed job again, with its attempts counted afresh, and appends the job_retry leaf build_leaf makes in the same transaction.

A job that failed because the file kept changing kept its result: it is queued as a merge of that result with the file as it is now, which is the merge that was never finished.

Source

pub fn release_open_jobs(&self, now: OffsetDateTime) -> Result<usize>

Makes every open job claimable at once: a leased one is released, its holder being gone, and a queued one’s retry delay is dropped. Answers how many there are.

For the server draining the queue itself once no worker is left to: a revoked worker’s lease would otherwise hold its job until it ran out, and a job waiting out a delay the worker’s failure set has no worker left to wait for. Attempts still count, so a job that keeps failing here is failed all the same.

Not in the audit log: it changes no file, and what it frees is recorded around it — the revoke of the worker that held a lease before, and the server’s own job_claim of each job after.

Source

pub fn fail_open_jobs_audited( &self, why: &str, now: OffsetDateTime, build_leaf: impl Fn(u64, &str, &JobChange<'_>) -> Vec<u8>, ) -> Result<Vec<String>>

Marks every open job failed, with why as its error, for a queue nothing is left to drain. Each keeps its input, so a retry merges it once something can. Answers their ids.

Each job gets a job_result leaf, which build_leaf makes, in the same transaction, as a lease that ran out on a last attempt does.

Source

pub fn queue_status(&self) -> Result<QueueStatus>

What /health says about the queue.

Source

pub fn prune_jobs(&self, before: &str) -> Result<usize>

Removes jobs that finished before before. Failed jobs stay until someone retries them: each may hold a result nobody has seen.

Not in the audit log: the leaves of the jobs removed stay in it.

Source§

impl Store

Source

pub fn has_admin_credentials(&self) -> Result<bool>

Whether the owner has registered any passkey.

Source

pub fn add_admin_credential_audited( &self, c: &NewAdminCredential<'_>, first: Option<FirstPasskey<'_>>, build_leaf: impl FnOnce(u64, &str) -> Vec<u8>, ) -> Result<AddedCredential>

Stores a passkey, with the passkey_add leaf build_leaf makes, in one transaction.

With first, only while none is stored, and only with the bootstrap code outstanding, which it uses up. All three are one transaction, so two bootstraps at once cannot both be the first, nor one code register two passkeys. It takes the write lock before anything is read, as every audited write does: reset-passkeys writes from another process.

Source

pub fn check_bootstrap_code( &self, code_sha256: &str, now: &str, ) -> Result<BootstrapCode>

Whether code_sha256 is the bootstrap code outstanding at now.

Source

pub fn set_bootstrap_code_audited( &self, code_sha256: &str, now: &str, expires_at: &str, build_leaf: impl FnOnce(u64, &str) -> Vec<u8>, ) -> Result<()>

Makes code_sha256 the one bootstrap code, until expires_at, replacing any other, with the bootstrap_code leaf build_leaf makes.

Source

pub fn admin_credential(&self, id: &str) -> Result<Option<AdminCredential>>

One passkey.

Source

pub fn admin_credentials(&self) -> Result<Vec<AdminCredential>>

Every passkey, oldest first.

Source

pub fn record_admin_sign_in( &self, id: &str, sign_count: u32, passkey: &str, now: &str, ) -> Result<bool>

Records a sign-in, if its signature counter moved forward.

WebAuthn §7.2 step 22: when either the stored counter or the new one is nonzero, the new one must be greater, or two copies of the credential’s key may exist. Both zero is what a synced passkey reports every time, and is accepted. The check and the update are one statement, so two sign-ins racing with the same counter cannot both pass. Answers whether it was recorded; false is a refusal.

Source

pub fn remove_admin_credential_audited( &self, id: &str, build_leaf: impl FnOnce(u64, &str, &AdminCredential) -> Vec<u8>, ) -> Result<RemovedCredential>

Removes a passkey and every session it signed in, unless it is the last one, with the passkey_remove leaf build_leaf makes from it.

Source

pub fn reset_admin_credentials_audited( &self, code_sha256: &str, now: &str, expires_at: &str, build_leaf: impl FnOnce(u64, &str, usize) -> Vec<u8>, ) -> Result<usize>

Removes every passkey and every session, and makes code_sha256 the bootstrap code until expires_at: what recall-server reset-passkeys does, for an owner who has lost them all. One transaction with the passkey_reset leaf build_leaf makes from how many passkeys went, which it answers, and it creates the tables first if this database has never been opened by a server that has them.

Run from another process than the server’s, on the same file: the leaf takes the next seq the table has, and a running server reads it into its tree before its own next append (see Store::audited_each).

Source

pub fn create_admin_session( &self, token_sha256: &str, credential_id: &str, now: &str, expires_at: &str, ) -> Result<bool>

Stores a new session, only if the passkey that signed it in is still there: a sign-in that finishes as its passkey is removed must not outlive it.

Source

pub fn admin_session(&self, token_sha256: &str) -> Result<Option<AdminSession>>

One session, by the hash of its token.

Source

pub fn touch_admin_session(&self, token_sha256: &str, now: &str) -> Result<()>

Records that a session was used, which is what keeps it from going idle.

Source

pub fn delete_other_admin_sessions_audited( &self, keep: &str, build_leaf: impl FnOnce(u64, &str, usize) -> Vec<u8>, ) -> Result<usize>

Ends every session but keep, with the sessions_end leaf build_leaf makes from how many ended, which it answers. Ending none changes nothing and appends nothing.

Source

pub fn delete_admin_session(&self, token_sha256: &str) -> Result<()>

Ends a session.

Source

pub fn sweep_admin_sessions( &self, now: &str, idle_before: &str, ) -> Result<usize>

Removes sessions past their absolute limit, or idle since before idle_before. Answers how many went.

Source§

impl Store

Source

pub fn open(path: impl AsRef<Path>) -> Result<Self>

Opens (creating if needed) the database at path, switching it to SQLite’s WAL journal with every commit synced before it returns (see use_durable_wal in this module for why both). A file an older server wrote with the rollback journal is converted here, once; the mode is stored in the file. Fails, rather than serving in another mode, if the switch cannot be made: the file is held by another process for longer than the busy timeout, or SQLite cannot keep the WAL’s index beside it. A network filesystem may well not fail here and still not work; deploy/README.md says not to use one.

Source

pub fn open_in_memory() -> Result<Self>

An in-memory database, for tests.

Source

pub fn get( &self, project_key: &str, file_path: &str, ) -> Result<Option<Existing>>

Reads one row.

Source

pub fn upsert_audited( &self, project_key: &str, file_path: &str, content: &str, source_env: &str, build_leaf: impl FnOnce(u64, &str) -> Vec<u8>, ) -> Result<String>

Writes content, clearing any tombstone, and appends the leaf build_leaf makes from its seq and at in the same transaction. Answers the time it was written, which is that at: the row’s updated_at and its leaf’s at are one timestamp.

Source

pub fn tombstone_audited( &self, project_key: &str, file_path: &str, source_env: &str, build_leaf: impl FnOnce(u64, &str) -> Vec<u8>, ) -> Result<String>

Marks a file deleted while deliberately leaving its content in place: a mistaken delete stays recoverable at the database level, even though nothing in the app surfaces an undo yet. Store::list withholds the content so a pull can’t resurrect it.

In the same transaction it closes the file’s open merge jobs, so nothing merges the deleted notes back into a file pushed after it, and appends its leaf, answering the time, as Store::upsert_audited does.

Source

pub fn list(&self, project_key: &str) -> Result<Vec<File>>

Every file for a project, tombstones included so a pulling client knows what to remove locally — but with the deleted content withheld.

Source

pub fn last_sync_at(&self) -> Result<String>

The most recent write across all projects, for /health. Empty when nothing has ever been synced.

Source

pub fn admin_stats(&self) -> Result<(Vec<ProjectStats>, AdminTotals)>

Aggregates per project for the admin page.

Source

pub fn checkpoint(&self) -> Result<bool>

Copies the commits the WAL holds into the database file, as far as no reader still needs them, waiting on nothing. Answers whether that was all of them.

SQLite already does this by itself once the WAL passes 1000 pages, which on a personal server can be days of pushes. The server also does it every sweep, so that recall.db on its own, all a reader that cannot see recall.db-wal has (sqlite-web’s single-file mount in deploy/docker-compose.direct.yml), is never far behind. Nothing relies on it for correctness: the WAL is part of the database. A reader that keeps one transaction open holds back everything committed after it began, and the WAL grows until it lets go; the server says so when that lasts several sweeps.

Source

pub fn checkpoint_all(&self) -> Result<bool>

Copies every commit the WAL holds into the database file and empties the WAL, waiting up to the busy timeout for a reader in the way. Answers whether it got all the way.

The last thing a server does as it stops. Closing the connection does the same, but only when no other process has the file open, and sqlite-web keeps it open. Emptied here, the WAL left beside the file holds nothing, so a stopped server’s recall.db is the whole database. Not something to rely on after a crash, which is why every procedure in deploy/README.md moves or copies the WAL with the file.

Source

pub fn backup(&self, dir: impl AsRef<Path>, keep: usize) -> Result<PathBuf>

Writes a consistent snapshot via VACUUM INTO, then prunes the oldest snapshots beyond keep.

VACUUM INTO reads through this connection, so the snapshot holds every commit, those still only in the WAL included, as of one moment, whatever is writing meanwhile. Copying recall.db instead would miss whatever the WAL has not yet handed back to it. And what it writes is one self-contained file in the rollback journal’s mode, with no -wal of its own, so a snapshot can be copied, uploaded, opened read-only, or copied over recall.db in a restore, as it is.

Auto Trait Implementations§

§

impl !Freeze for Store

§

impl RefUnwindSafe for Store

§

impl Send for Store

§

impl Sync for Store

§

impl Unpin for Store

§

impl UnsafeUnpin for Store

§

impl UnwindSafe for Store

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more