silicon-remind-client#
A stateless Rust client for Silicon Remind: sign a Carbon or Silicon in with Silicon Accounts,
then create and read reminders, share them, manage webhook subscriptions and test environments.
It has no dependency on the Remind service or its database. The remind CLI
is built only on this crate, so everything the CLI does, your program can do.
[dependencies]
silicon-remind-client = "0.6"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Rust 1.98 or newer. Version 0.6 speaks Remind's API contract 2 (/api/v2) and signs in with
Silicon Accounts; 0.5 and earlier spoke the retired contract 1.
Two parts#
accounts::SignInsigns an account in to Remind at Silicon Accounts, as Remind's own public client: no secret, only the app idremind. It returnsaccounts::Tokens: an access token (30 minutes), a rotating refresh token and the account. Where you keep them is up to you.Clientcalls the Remind API with an access token (with_session), or, for another app reading on an account's behalf, a User verification proof (with_proof).with_test_environmentselects a test environment's data without changing any method.
Both are immutable configuration: every with_* method returns a new value, nothing is cached,
nothing refreshes by itself, and constructing one makes no network call. Origins must be https;
plain http is accepted only for localhost, 127.0.0.1 and ::1. Redirects are not followed,
requests time out after 30 seconds (connections after 5), and response bodies are capped at
16 MiB.
Sign in a Silicon#
A Silicon mints a short-lived token for Remind (silicon-accounts login --app remind -q) and
hands it to your program:
use silicon_remind_client::{Client, Mutation, Secret, accounts::{SignIn, DEFAULT_ACCOUNTS_URL}, models};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let sign_in = SignIn::new(DEFAULT_ACCOUNTS_URL, "remind")?;
let slt = Secret::new(std::env::var("REMIND_SLT")?); // slt_…: single use, 2 minutes
let tokens = sign_in.exchange_slt(&slt).await?;
println!("signed in as {} ({})", tokens.account.id, tokens.account.uuid);
let remind = Client::new(silicon_remind_client::DEFAULT_URL)?
.with_session(tokens.access_token.clone())?;
let reminder = remind
.create_reminder(
&models::CreateScheduleRequest {
text: "Review the build results".into(),
kind: models::ScheduleKind::Recurring,
cron: "*/15 * * * *".into(),
timezone: "UTC".into(),
},
&Mutation::new(),
)
.await?;
println!("created {}", reminder.id);
Ok(())
}
A refused token is an Error::SignInRefused whose refusal says why:
SltAlreadyUsed, SltExpired, SltWrongApp { app }, SltUnknown, NotAShortLivedToken
(nothing was sent) or Other; its hint says to mint a fresh one with
silicon-accounts login --app remind -q. Every refused token is used up.
Sign in a Carbon (device flow)#
use silicon_remind_client::accounts::{DeviceProgress, SignIn};
async fn carbon(sign_in: &SignIn) -> silicon_remind_client::Result<()> {
let device = sign_in.start_device(Some("my tool on build-box")).await?;
println!("Open {} and enter {}", device.verification_uri, device.user_code);
let tokens = sign_in
.wait_for_device(&device, |progress| {
if let DeviceProgress::SlowDown { interval } = progress {
eprintln!("checking every {interval}s");
}
})
.await?;
println!("signed in as {}", tokens.account.id);
Ok(())
}
wait_for_device polls as Silicon Accounts allows (interval, plus 5 seconds after each
slow_down), retries passing network failures, and ends with Refusal::DeviceDenied or
Refusal::DeviceExpired (codes last 10 minutes). Use poll_device to drive polling yourself.
Remind's sign-in setup must have device sign-in on (Refusal::NotAllowed otherwise).
Keep the sign-in fresh, end it#
use silicon_remind_client::{Secret, accounts::SignIn};
async fn renew(sign_in: &SignIn, refresh_token: &Secret) -> silicon_remind_client::Result<()> {
let tokens = sign_in.refresh(refresh_token).await?; // store tokens.refresh_token BEFORE using it
let ended = sign_in.revoke(&tokens.refresh_token).await?; // sign-out; false: it was already over
println!("revoked: {ended}");
Ok(())
}
Refresh tokens rotate on every use, and presenting a used one ends the whole sign-in
(Refusal::SignInEnded). Refresh one at a time (the CLI holds a file lock), store the new pair
before using it, and never retry a refresh that may have reached Silicon Accounts with the same
token. Tokens::expires_at is in Unix seconds by this machine's clock; refresh a little before.
Read on someone's behalf (other apps)#
use silicon_remind_client::{Client, Secret, models};
async fn read_for(proof: Secret) -> silicon_remind_client::Result<()> {
let remind = Client::new(silicon_remind_client::DEFAULT_URL)?.with_proof(proof)?; // sap_…
let page = remind.reminders(&models::ListSchedules::default()).await?;
println!("{} reminders", page.items.len());
Ok(())
}
Remind accepts a User verification proof (Authorization: Proof sap_…) issued for receiving app
remind with the scope remind.schedules.read, from an app its deployment trusts, on the read
methods only: reminders, reminder, executions, silicons and me. See
signing in and who sees what.
State and secrets#
Secret hides its value from Debug output and gives it out only through expose(). Its
Serialize deliberately writes the value (request bodies, your own secure storage); never
serialize tokens into ordinary logs. Inside a test environment you stay the account you signed
in as: with_test_environment keeps the credential and only adds the key.
Mutation::new() makes a unique idempotency key; Mutation::with_key adopts your own (16 to 255
visible ASCII characters). Reuse the same Mutation when retrying the exact same create, edit,
status change or report after an uncertain failure. The client sends each call once; retrying is
your decision.
Method reference#
| method | what it does |
|---|---|
SignIn::new(accounts_url, app_id) |
sign in to app_id (normally "remind") at that Silicon Accounts |
start_device(label), poll_device(&device), wait_for_device(&device, progress) |
Carbon device sign-in |
exchange_slt(&slt) |
Silicon sign-in with a short-lived token |
refresh(&refresh_token) |
rotate the pair |
revoke(&refresh_token) |
sign out; true when something was revoked |
Client::new(url) |
client for the Remind API at a pathless origin |
with_session(access_token), with_proof(proof) |
attach a credential |
with_test_environment(key), with_telemetry(on) |
select a test environment; telemetry |
health(ready), versions() |
liveness/readiness; served contracts (no credential needed) |
me() |
the calling account as Remind sees it (Identity) |
login_status() |
LoginStatus: authenticated: false only for HTTP 401 |
create_reminder(input, mutation) |
create a reminder for the signed-in Silicon |
reminders(filters) |
ListSchedules → Page<ScheduleResponse> |
reminder(id) |
one reminder you can read |
update_reminder(id, patch, mutation) |
change text, cron, timezone, kind or status |
set_status(ids, status, mutation) |
pause or resume 1 to 100 reminders, all or nothing |
archive_reminder(id) |
archive one of your reminders (45 days) |
executions(id, paging) |
delivery history |
silicons(after, limit) |
Page<VisibleSilicon>: the Silicons you can read, with relation |
viewers(), grant_viewer(target), revoke_viewer(account, silicon) |
sharing (viewer grants) |
allowed_accounts(silicon), allow_account(target), disallow_account(account, silicon) |
a Silicon's allow-list |
subscribe_webhook(destination), webhooks(silicon), unsubscribe_webhook(id) |
delivery subscriptions |
configure_webhook(destination), webhook(), disable_webhook() |
older single-endpoint forms |
create_environment(input), environments(..), environment(id) |
test environments (from production) |
environment_key(id), rotate_environment_key(id), delete_environment(id), restore_environment(id) |
keys and lifecycle |
current_environment(), clean_environment() |
inside a test environment, by key alone |
report(input, mutation), report_status(id) |
bug reports |
track(event) |
one best-effort telemetry event (never fails the caller) |
Accounts are AccountRef { uuid, id, kind }: key on uuid (a permanent 128-bit UUIDv4 in
canonical lowercase, hyphenated form) and show id (c:ada, si:scout), which can change. A reminder
has owner; silicon_id is the owner's current id. AccountTarget { id, silicon_id } names
another account by c:/si: id or uuid, and, for a custodian, which of its Silicons the request
is about.
ListSchedules::default() selects current reminders; set section to Archived for the
archive. Pass a page's next_cursor unchanged, with the same filters, to get the next page.
PatchScheduleRequest fields are Options: None keeps the current value.
CreateScheduleRequest.timezone is mandatory (an IANA identifier such as Asia/Kolkata or
UTC); a blank one is refused before anything is sent.
Test environments#
use silicon_remind_client::{Client, Secret, models};
async fn try_it(access_token: Secret, key: Secret) -> silicon_remind_client::Result<()> {
let remind = Client::new("http://127.0.0.1:4181")?.with_session(access_token)?;
let created = remind
.create_environment(&models::CreateEnvironment { name: "release-qa".into(), description: None })
.await?;
let sandbox = remind.with_test_environment(created.key)?; // same account, isolated data
let page = sandbox.reminders(&models::ListSchedules::default()).await?;
println!("{}: {} reminders", created.environment.name, page.items.len());
let shared = Client::new("http://127.0.0.1:4181")?.with_test_environment(key)?;
println!("{}", shared.current_environment().await?.name); // by key alone
Ok(())
}
Managing environments needs a client without a key (Error::Invalid otherwise);
current_environment and clean_environment need one. See testing.
Errors#
Every error has code(), message(), hint(), status() and request_id(), and
is_transient() says whether a later retry may work.
Error::Apiis Remind's own error body{"error":{"code","message","hint"?,"request_id"}}with the HTTP status andretry_after. 401: no or ended sign-in (token_revoked,token_expired,account_deleted,token_wrong_audience…); 403: not allowed (not_reminder_owner,silicon_only,not_custodian…); 404 also hides what you cannot see; 409: a state or idempotency conflict; 410api_version_retired: a contract 1 client.Error::SignInRefused { refusal, message, hint }: Silicon Accounts refused a sign-in.Error::Accounts: any other Silicon Accounts failure, or it could not be reached (connection_failed,request_timeout).Error::Invalid(refused locally),Error::Transport(no answer from Remind),Error::Decode(an answer this client does not understand),Error::ResponseTooLarge.
Versions, reports and telemetry#
versions() reads /api/versions; every request names contract 2 (X-Remind-API-Version: 2).
See the version policy. report and report_status send and track a
bug report. Telemetry is on by default: with_telemetry(false) turns off client events and
Remind's request observations for that client. Events go through the Remind API; no Space
Station key ships with the client. See diagnostics.
The crate never updates itself: update it in your Cargo.toml and rebuild.