Merge branch 'master' into tyler/error-callback

This commit is contained in:
Tyler Cloutier
2025-02-11 00:41:16 -05:00
committed by GitHub
42 changed files with 759 additions and 623 deletions
Generated
+5
View File
@@ -4884,6 +4884,7 @@ dependencies = [
"is-terminal",
"itertools 0.12.1",
"mimalloc",
"percent-encoding",
"regex",
"reqwest 0.12.9",
"rustyline",
@@ -4959,6 +4960,9 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tower-http",
"tower-layer",
"tower-service",
"tracing",
"uuid",
]
@@ -4979,6 +4983,7 @@ dependencies = [
"proptest",
"serde",
"serde_json",
"serde_with",
"smallvec",
"spacetimedb-lib",
"spacetimedb-primitives",
+3
View File
@@ -192,6 +192,7 @@ nohash-hasher = "0.2"
once_cell = "1.16"
parking_lot = { version = "0.12.1", features = ["send_guard", "arc_lock"] }
paste = "1.0"
percent-encoding = "2.3"
petgraph = { version = "0.6.5", default-features = false }
pin-project-lite = "0.2.9"
postgres-types = "0.2.5"
@@ -249,6 +250,8 @@ tokio-util = { version = "0.7.4", features = ["time"] }
toml = "0.8"
toml_edit = "0.22.22"
tower-http = { version = "0.5", features = ["cors"] }
tower-layer = "0.3"
tower-service = "0.3"
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-core = "0.1.31"
+1
View File
@@ -47,6 +47,7 @@ itertools.workspace = true
indicatif.workspace = true
jsonwebtoken.workspace = true
mimalloc.workspace = true
percent-encoding.workspace = true
regex.workspace = true
reqwest.workspace = true
rustyline.workspace = true
+6 -7
View File
@@ -7,7 +7,7 @@ use spacetimedb_lib::de::serde::DeserializeWrapper;
use spacetimedb_lib::sats::ProductType;
use spacetimedb_lib::Identity;
use crate::util::AuthHeader;
use crate::util::{AuthHeader, ResponseExt};
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
@@ -23,10 +23,10 @@ impl Connection {
pub fn db_uri(&self, endpoint: &str) -> String {
[
&self.host,
"/database/",
endpoint,
"/",
"/v1/database/",
&self.database_identity.to_hex(),
"/",
endpoint,
]
.concat()
}
@@ -66,9 +66,8 @@ impl ClientApi {
.get(self.con.db_uri("schema"))
.query(&[("version", "9")])
.send()
.await?
.error_for_status()?;
let DeserializeWrapper(module_def) = res.json().await?;
.await?;
let DeserializeWrapper(module_def) = res.json_or_error().await?;
Ok(module_def)
}
+4 -4
View File
@@ -163,15 +163,15 @@ impl RawConfig {
nickname: Some("local".to_string()),
ecdsa_public_key: None,
};
let testnet = ServerConfig {
host: "testnet.spacetimedb.com".to_string(),
let maincloud = ServerConfig {
host: "maincloud.spacetimedb.com".to_string(),
protocol: "https".to_string(),
nickname: Some("testnet".to_string()),
nickname: Some("maincloud".to_string()),
ecdsa_public_key: None,
};
RawConfig {
default_server: local.nickname.clone(),
server_configs: vec![local, testnet],
server_configs: vec![local, maincloud],
web_session_token: None,
spacetimedb_token: None,
}
-31
View File
@@ -1,22 +1,7 @@
use reqwest::{Response, StatusCode};
use thiserror::Error;
#[derive(Debug)]
pub enum RequestSource {
Client,
Server,
}
#[derive(Error, Debug)]
pub enum CliError {
#[error("HTTP status {kind:?} error ({status}): {msg}")]
Request {
msg: String,
kind: RequestSource,
status: StatusCode,
},
#[error(transparent)]
ReqWest(#[from] reqwest::Error),
#[error("Config error: The option `{key}` not found")]
Config { key: String },
#[error("Config error: The option `{key}` is not a `{kind}`, found: `{type}: {value}`",
@@ -29,19 +14,3 @@ pub enum CliError {
found: Box<toml_edit::Item>,
},
}
/// Turn a response into an error if the server returned an error.
pub async fn error_for_status(response: Response) -> Result<Response, CliError> {
let status = response.status();
if let Some(kind) = status
.is_client_error()
.then_some(RequestSource::Client)
// Anything that is not a success is an error for the client, even a redirect that is not followed.
.or_else(|| (!status.is_success()).then_some(RequestSource::Server))
{
let msg = response.text().await?;
return Err(CliError::Request { kind, msg, status });
}
Ok(response)
}
+1 -1
View File
@@ -23,7 +23,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let identity = database_identity(&config, database, server).await?;
let builder = reqwest::Client::new().post(format!("{}/database/delete/{}", config.get_host_url(server)?, identity));
let builder = reqwest::Client::new().delete(format!("{}/v1/database/{}", config.get_host_url(server)?, identity));
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
let builder = add_auth_header_opt(builder, &auth_header);
builder.send().await?.error_for_status()?;
+10 -28
View File
@@ -1,13 +1,10 @@
use crate::common_args;
use crate::config::Config;
use crate::util::{
add_auth_header_opt, decode_identity, get_auth_header, get_login_token_or_log_in, spacetime_register_tld,
};
use crate::util::{add_auth_header_opt, decode_identity, get_auth_header, get_login_token_or_log_in, ResponseExt};
use clap::ArgMatches;
use clap::{Arg, Command};
use reqwest::Url;
use spacetimedb_client_api_messages::name::{InsertDomainResult, RegisterTldResult};
use spacetimedb_client_api_messages::name::{DomainName, InsertDomainResult};
pub fn cli() -> Command {
Command::new("rename")
@@ -37,32 +34,17 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let identity = decode_identity(&token)?;
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
match spacetime_register_tld(&mut config, domain, server, !force).await? {
RegisterTldResult::Success { domain } => {
println!("Registered domain: {}", domain);
}
RegisterTldResult::Unauthorized { domain } => {
return Err(anyhow::anyhow!("Domain is already registered by another: {}", domain));
}
RegisterTldResult::AlreadyRegistered { domain } => {
println!("Domain is already registered by the identity you provided: {}", domain);
}
}
let domain: DomainName = domain.parse()?;
let builder = reqwest::Client::new().get(Url::parse_with_params(
format!("{}/database/set_name", config.get_host_url(server)?).as_str(),
[
("domain", domain.clone()),
("database_identity", database_identity.clone()),
("register_tld", "true".to_string()),
],
)?);
let builder = reqwest::Client::new()
.post(format!(
"{}/v1/database/{database_identity}/names",
config.get_host_url(server)?
))
.body(String::from(domain));
let builder = add_auth_header_opt(builder, &auth_header);
let res = builder.send().await?.error_for_status()?;
let bytes = res.bytes().await.unwrap();
println!("{}", String::from_utf8_lossy(&bytes[..]));
let result: InsertDomainResult = serde_json::from_slice(&bytes[..]).unwrap();
let result = builder.send().await?.json_or_error().await?;
match result {
InsertDomainResult::Success {
domain,
+1 -1
View File
@@ -60,7 +60,7 @@ async fn exec_status(mut config: Config, args: &ArgMatches) -> Result<(), anyhow
};
let status = reqwest::Client::new()
.get(format!("{}/energy/{}", config.get_host_url(server)?, identity))
.get(format!("{}/v1/energy/{}", config.get_host_url(server)?, identity))
.send()
.await?
.error_for_status()?
@@ -348,23 +348,23 @@ removeOnUpdate = (cb: (ctx: EventContext, onRow: {row_type}, newRow: {row_type})
writeln!(out, "}},");
writeln!(
out,
"// Constructors which are used by the DBConnectionImpl to
"// Constructors which are used by the DbConnectionImpl to
// extract type information from the generated RemoteModule.
//
// NOTE: This is not strictly necessary for `eventContextConstructor` because
// all we do is build a TypeScript object which we could have done inside the
// SDK, but if in the future we wanted to create a class this would be
// necessary because classes have methods, so we'll keep it.
eventContextConstructor: (imp: DBConnectionImpl, event: Event<Reducer>) => {{
eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {{
return {{
...(imp as DBConnection),
...(imp as DbConnection),
event
}}
}},
dbViewConstructor: (imp: DBConnectionImpl) => {{
dbViewConstructor: (imp: DbConnectionImpl) => {{
return new RemoteTables(imp);
}},
reducersConstructor: (imp: DBConnectionImpl, setReducerFlags: SetReducerFlags) => {{
reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {{
return new RemoteReducers(imp, setReducerFlags);
}},
setReducerFlagsConstructor: () => {{
@@ -433,7 +433,7 @@ fn print_remote_reducers(module: &ModuleDef, out: &mut Indenter) {
out.indent(1);
writeln!(
out,
"constructor(private connection: DBConnectionImpl, private setCallReducerFlags: SetReducerFlags) {{}}"
"constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {{}}"
);
out.newline();
@@ -537,7 +537,7 @@ fn print_set_reducer_flags(module: &ModuleDef, out: &mut Indenter) {
fn print_remote_tables(module: &ModuleDef, out: &mut Indenter) {
writeln!(out, "export class RemoteTables {{");
out.indent(1);
writeln!(out, "constructor(private connection: DBConnectionImpl) {{}}");
writeln!(out, "constructor(private connection: DbConnectionImpl) {{}}");
for table in iter_tables(module) {
writeln!(out);
@@ -571,12 +571,12 @@ fn print_subscription_builder(_module: &ModuleDef, out: &mut Indenter) {
fn print_db_connection(_module: &ModuleDef, out: &mut Indenter) {
writeln!(
out,
"export class DBConnection extends DBConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{"
"export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{"
);
out.indent(1);
writeln!(
out,
"static builder = (): DBConnectionBuilder<DBConnection, ErrorContext, SubscriptionEventContext> => {{"
"static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {{"
);
out.indent(1);
writeln!(
@@ -620,7 +620,7 @@ fn print_spacetimedb_imports(out: &mut Indenter) {
"ConnectionId",
"Timestamp",
"TimeDuration",
"DBConnectionBuilder",
"DbConnectionBuilder",
"TableCache",
"BinaryWriter",
"CallReducerFlags",
@@ -630,8 +630,8 @@ fn print_spacetimedb_imports(out: &mut Indenter) {
"ErrorContextInterface",
"SubscriptionBuilderImpl",
"BinaryReader",
"DBConnectionImpl",
"DBContext",
"DbConnectionImpl",
"DbContext",
"Event",
"deepEqual",
];
+7 -10
View File
@@ -1,10 +1,11 @@
use crate::common_args;
use crate::util;
use crate::util::get_login_token_or_log_in;
use crate::util::ResponseExt;
use crate::util::UNSTABLE_WARNING;
use crate::Config;
use anyhow::Context;
use clap::{ArgMatches, Command};
use reqwest::StatusCode;
use serde::Deserialize;
use spacetimedb::Identity;
use tabled::{
@@ -44,7 +45,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let client = reqwest::Client::new();
let res = client
.get(format!(
"{}/identity/{}/databases",
"{}/v1/identity/{}/databases",
config.get_host_url(server)?,
identity
))
@@ -52,14 +53,10 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
.send()
.await?;
if res.status() != StatusCode::OK {
return Err(anyhow::anyhow!(format!(
"Unable to retrieve databases for identity: {}",
res.status()
)));
}
let result: DatabasesResult = res.json().await?;
let result: DatabasesResult = res
.json_or_error()
.await
.context("unable to retrieve databases for identity")?;
if !result.identities.is_empty() {
let mut table = Table::new(result.identities);
+7 -1
View File
@@ -273,6 +273,12 @@ struct LocalLoginResponse {
async fn spacetimedb_direct_login(host: &Url) -> Result<String, anyhow::Error> {
let client = reqwest::Client::new();
let response: LocalLoginResponse = client.post(host.join("identity")?).send().await?.json().await?;
let response: LocalLoginResponse = client
.post(host.join("/v1/identity")?)
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(response.token)
}
+1 -1
View File
@@ -130,7 +130,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let host_url = config.get_host_url(server)?;
let builder = reqwest::Client::new().get(format!("{}/database/logs/{}", host_url, database_identity));
let builder = reqwest::Client::new().get(format!("{}/v1/database/{}/logs", host_url, database_identity));
let builder = add_auth_header_opt(builder, &auth_header);
let mut res = builder.query(&query_parms).send().await?;
let status = res.status();
+17 -51
View File
@@ -1,4 +1,3 @@
use anyhow::bail;
use clap::Arg;
use clap::ArgAction::{Set, SetTrue};
use clap::ArgMatches;
@@ -9,7 +8,7 @@ use std::fs;
use std::path::PathBuf;
use crate::config::Config;
use crate::util::{add_auth_header_opt, get_auth_header};
use crate::util::{add_auth_header_opt, get_auth_header, ResponseExt};
use crate::util::{decode_identity, unauth_error_context, y_or_n};
use crate::{build, common_args};
@@ -82,18 +81,19 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
// easily create a new identity with an email
let auth_header = get_auth_header(&mut config, anon_identity, server, !force).await?;
let mut query_params = Vec::<(&str, &str)>::new();
query_params.push(("host_type", "wasm"));
query_params.push(("register_tld", "true"));
let client = reqwest::Client::new();
// If a domain or identity was provided, we should locally make sure it looks correct and
// append it as a query parameter
if let Some(name_or_identity) = name_or_identity {
let mut builder = if let Some(name_or_identity) = name_or_identity {
if !is_identity(name_or_identity) {
parse_domain_name(name_or_identity)?;
}
query_params.push(("name_or_identity", name_or_identity.as_str()));
}
let encode_set = const { &percent_encoding::NON_ALPHANUMERIC.remove(b'_').remove(b'-') };
let domain = percent_encoding::percent_encode(name_or_identity.as_bytes(), encode_set);
client.put(format!("{database_host}/v1/database/{domain}"))
} else {
client.post(format!("{database_host}/v1/database"))
};
if !path_to_project.exists() {
return Err(anyhow::anyhow!(
@@ -145,16 +145,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
println!("Aborting");
return Ok(());
}
query_params.push(("clear", "true"));
builder = builder.query(&[("clear", true)]);
}
println!("Publishing module...");
let mut builder = reqwest::Client::new().post(Url::parse_with_params(
format!("{}/database/publish", database_host).as_str(),
query_params,
)?);
builder = add_auth_header_opt(builder, &auth_header);
let res = builder.body(program_bytes).send().await?;
@@ -169,13 +164,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
config.server_nick_or_host(server)?,
);
}
if res.status().is_client_error() || res.status().is_server_error() {
let err = res.text().await?;
bail!(err)
}
let bytes = res.bytes().await.unwrap();
let response: PublishResult = serde_json::from_slice(&bytes[..]).unwrap();
let response: PublishResult = res.json_or_error().await?;
match response {
PublishResult::Success {
domain,
@@ -192,20 +182,9 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
println!("{} database with identity: {}", op, database_identity);
}
}
PublishResult::TldNotRegistered { domain } => {
return Err(anyhow::anyhow!(
"The top level domain that you provided is not registered.\n\
This tld is not yet registered to any identity: {}",
domain.tld()
));
}
PublishResult::PermissionDenied { domain } => {
PublishResult::PermissionDenied { name } => {
if anon_identity {
anyhow::bail!(
"You need to be logged in as the owner of {} to publish to {}",
domain.tld(),
domain.tld()
);
anyhow::bail!("You need to be logged in as the owner of {name} to publish to {name}",);
}
// If we're not in the `anon_identity` case, then we have already forced the user to log in above (using `get_auth_header`), so this should be safe to unwrap.
let token = config.spacetimedb_token().unwrap();
@@ -213,24 +192,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
//TODO(jdetter): Have a nice name generator here, instead of using some abstract characters
// we should perhaps generate fun names like 'green-fire-dragon' instead
let suggested_tld: String = identity.chars().take(12).collect();
if let Some(sub_domain) = domain.sub_domain() {
return Err(anyhow::anyhow!(
"The top level domain {} is not registered to the identity you provided.\n\
We suggest you publish to a domain that starts with a TLD owned by you, or publish to a new domain like:\n\
\tspacetime publish {}/{}\n",
domain.tld(),
suggested_tld,
sub_domain
));
} else {
return Err(anyhow::anyhow!(
"The top level domain {} is not registered to the identity you provided.\n\
return Err(anyhow::anyhow!(
"The database {name} is not registered to the identity you provided.\n\
We suggest you push to either a domain owned by you, or a new domain like:\n\
\tspacetime publish {}\n",
domain.tld(),
suggested_tld
));
}
\tspacetime publish {suggested_tld}\n",
));
}
}
+1 -1
View File
@@ -292,7 +292,7 @@ pub async fn exec_ping(config: Config, args: &ArgMatches) -> Result<(), anyhow::
let server = args.get_one::<String>("server").unwrap().as_str();
let url = config.get_host_url(Some(server))?;
let builder = reqwest::Client::new().get(format!("{}/database/ping", url).as_str());
let builder = reqwest::Client::new().get(format!("{}/v1/ping", url).as_str());
let response = builder.send().await?;
match response.status() {
+8 -4
View File
@@ -2,6 +2,7 @@ use std::time::Instant;
use crate::api::{from_json_seed, ClientApi, Connection, StmtResultJson};
use crate::common_args;
use anyhow::Context;
use clap::{Arg, ArgAction, ArgMatches};
use itertools::Itertools;
use reqwest::RequestBuilder;
@@ -10,8 +11,7 @@ use spacetimedb_lib::sats::{satn, Typespace};
use tabled::settings::Style;
use crate::config::Config;
use crate::errors::error_for_status;
use crate::util::{database_identity, get_auth_header, UNSTABLE_WARNING};
use crate::util::{database_identity, get_auth_header, ResponseExt, UNSTABLE_WARNING};
pub fn cli() -> clap::Command {
clap::Command::new("sql")
@@ -67,12 +67,16 @@ fn print_timings(now: Instant) {
pub(crate) async fn run_sql(builder: RequestBuilder, sql: &str, with_stats: bool) -> Result<(), anyhow::Error> {
let now = Instant::now();
let json = error_for_status(builder.body(sql.to_owned()).send().await?)
let json = builder
.body(sql.to_owned())
.send()
.await?
.ensure_content_type("application/json")
.await?
.text()
.await?;
let stmt_result_json: Vec<StmtResultJson> = serde_json::from_str(&json)?;
let stmt_result_json: Vec<StmtResultJson> = serde_json::from_str(&json).context("malformed sql response")?;
// Print only `OK for empty tables as it's likely a command like `INSERT`.
if stmt_result_json.is_empty() {
+96 -36
View File
@@ -2,7 +2,7 @@ use anyhow::Context;
use base64::{engine::general_purpose::STANDARD_NO_PAD as BASE_64_STD_NO_PAD, Engine as _};
use reqwest::{RequestBuilder, Url};
use spacetimedb::auth::identity::{IncomingClaims, SpacetimeIdentityClaims};
use spacetimedb_client_api_messages::name::{DnsLookupResponse, RegisterTldResult, ReverseDNSResponse};
use spacetimedb_client_api_messages::name::GetNamesResponse;
use spacetimedb_lib::Identity;
use std::io::Write;
use std::path::Path;
@@ -21,9 +21,88 @@ pub async fn database_identity(
if let Ok(identity) = Identity::from_hex(name_or_identity) {
return Ok(identity);
}
match spacetime_dns(config, name_or_identity, server).await? {
DnsLookupResponse::Success { domain: _, identity } => Ok(identity),
DnsLookupResponse::Failure { domain } => Err(anyhow::anyhow!("The dns resolution of `{}` failed.", domain)),
spacetime_dns(config, name_or_identity, server)
.await?
.with_context(|| format!("the dns resolution of `{name_or_identity}` failed."))
}
pub(crate) trait ResponseExt: Sized {
/// Ensure that this response has the given content-type, especially if it's
/// a success response.
///
/// This checks the response status for you, so you shouldn't call
/// `error_for_status()` beforehand.
///
/// If the response does not have the given content type, assume it's an error message
/// and return it as such. Success responses with the wrong content type are treated
/// as a bug in the API implementation, since that makes it harder to tell what's
/// meant to be a structured response and what's a plain-text error message.
async fn ensure_content_type(self, content_type: &str) -> anyhow::Result<Self>;
/// Like [`reqwest::Response::json()`], but handles non-JSON error messages gracefully.
async fn json_or_error<T: serde::de::DeserializeOwned>(self) -> anyhow::Result<T>;
/// Transforms a status of `NOT_FOUND` into `None`.
fn found(self) -> Option<Self>;
}
fn err_status_desc(status: http::StatusCode) -> Option<&'static str> {
if status.is_success() {
None
} else if status.is_client_error() {
Some("HTTP status client error")
} else if status.is_server_error() {
Some("HTTP status server error")
} else {
Some("unexpected HTTP status code")
}
}
impl ResponseExt for reqwest::Response {
async fn ensure_content_type(self, content_type: &str) -> anyhow::Result<Self> {
let status = self.status();
if self
.headers()
.get(http::header::CONTENT_TYPE)
.is_some_and(|ty| ty == content_type)
{
return Ok(self);
}
let url = self.url();
let Some(status_desc) = err_status_desc(status) else {
anyhow::bail!("HTTP response from url ({url}) was success but did not have content-type: {content_type}");
};
let url = url.to_string();
let status_err = match self.error_for_status_ref() {
Err(e) => anyhow::Error::from(e),
Ok(_) => anyhow::anyhow!("{status_desc} ({status}) from url ({url})"),
};
let err = match self.text().await {
Ok(text) => status_err.context(text),
Err(err) => anyhow::Error::from(err)
.context(format!("{status_desc} ({status})"))
.context("failed to get response text"),
};
Err(err)
}
async fn json_or_error<T: serde::de::DeserializeOwned>(self) -> anyhow::Result<T> {
let status = self.status();
self.ensure_content_type("application/json")
.await?
.json()
.await
.map_err(|err| {
let mut err = anyhow::Error::from(err);
if let Some(desc) = err_status_desc(status) {
err = err.context(format!("malformed json payload for {desc} ({status})"))
}
err
})
}
fn found(self) -> Option<Self> {
(self.status() != http::StatusCode::NOT_FOUND).then_some(self)
}
}
@@ -32,37 +111,20 @@ pub async fn spacetime_dns(
config: &Config,
domain: &str,
server: Option<&str>,
) -> Result<DnsLookupResponse, anyhow::Error> {
) -> Result<Option<Identity>, anyhow::Error> {
let client = reqwest::Client::new();
let url = format!("{}/database/dns/{}", config.get_host_url(server)?, domain);
let res = client.get(url).send().await?.error_for_status()?;
let bytes = res.bytes().await.unwrap();
Ok(serde_json::from_slice(&bytes[..]).unwrap())
}
/// Registers the given top level domain to the given identity. If None is passed in as identity, the default
/// identity will be looked up in the config and it will be used instead. Returns Ok() if the
/// domain is successfully registered, returns Err otherwise.
pub async fn spacetime_register_tld(
config: &mut Config,
tld: &str,
server: Option<&str>,
interactive: bool,
) -> Result<RegisterTldResult, anyhow::Error> {
let auth_header = get_auth_header(config, false, server, interactive).await?;
// TODO(jdetter): Fix URL encoding on specifying this domain
let builder = reqwest::Client::new()
.get(format!("{}/database/register_tld?tld={}", config.get_host_url(server)?, tld).as_str());
let builder = add_auth_header_opt(builder, &auth_header);
let res = builder.send().await?.error_for_status()?;
let bytes = res.bytes().await.unwrap();
Ok(serde_json::from_slice(&bytes[..]).unwrap())
let url = format!("{}/v1/database/{}/identity", config.get_host_url(server)?, domain);
let Some(res) = client.get(url).send().await?.found() else {
return Ok(None);
};
let text = res.error_for_status()?.text().await?;
text.parse()
.map(Some)
.context("identity endpoint did not return an identity")
}
pub async fn spacetime_server_fingerprint(url: &str) -> anyhow::Result<String> {
let builder = reqwest::Client::new().get(format!("{}/identity/public-key", url).as_str());
let builder = reqwest::Client::new().get(format!("{}/v1/identity/public-key", url).as_str());
let res = builder.send().await?.error_for_status()?;
let fingerprint = res.text().await?;
Ok(fingerprint)
@@ -73,12 +135,10 @@ pub async fn spacetime_reverse_dns(
config: &Config,
identity: &str,
server: Option<&str>,
) -> Result<ReverseDNSResponse, anyhow::Error> {
) -> Result<GetNamesResponse, anyhow::Error> {
let client = reqwest::Client::new();
let url = format!("{}/database/reverse_dns/{}", config.get_host_url(server)?, identity);
let res = client.get(url).send().await?.error_for_status()?;
let bytes = res.bytes().await.unwrap();
Ok(serde_json::from_slice(&bytes[..]).unwrap())
let url = format!("{}/v1/database/{}/names", config.get_host_url(server)?, identity);
client.get(url).send().await?.json_or_error().await
}
/// Add an authorization header, if provided, to the request `builder`.
@@ -16,9 +16,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -79,9 +79,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -142,9 +142,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -207,9 +207,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -267,9 +267,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -330,9 +330,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -390,9 +390,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -453,9 +453,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -516,9 +516,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -594,9 +594,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -672,9 +672,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -737,9 +737,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -962,23 +962,23 @@ const REMOTE_MODULE = {
argsType: TestBtreeIndexArgs.getTypeScriptAlgebraicType(),
},
},
// Constructors which are used by the DBConnectionImpl to
// Constructors which are used by the DbConnectionImpl to
// extract type information from the generated RemoteModule.
//
// NOTE: This is not strictly necessary for `eventContextConstructor` because
// all we do is build a TypeScript object which we could have done inside the
// SDK, but if in the future we wanted to create a class this would be
// necessary because classes have methods, so we'll keep it.
eventContextConstructor: (imp: DBConnectionImpl, event: Event<Reducer>) => {
eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {
return {
...(imp as DBConnection),
...(imp as DbConnection),
event
}
},
dbViewConstructor: (imp: DBConnectionImpl) => {
dbViewConstructor: (imp: DbConnectionImpl) => {
return new RemoteTables(imp);
},
reducersConstructor: (imp: DBConnectionImpl, setReducerFlags: SetReducerFlags) => {
reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {
return new RemoteReducers(imp, setReducerFlags);
},
setReducerFlagsConstructor: () => {
@@ -1005,7 +1005,7 @@ export type Reducer = never
;
export class RemoteReducers {
constructor(private connection: DBConnectionImpl, private setCallReducerFlags: SetReducerFlags) {}
constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {}
add(name: string, age: number) {
const __args = { name, age };
@@ -1274,7 +1274,7 @@ export class SetReducerFlags {
}
export class RemoteTables {
constructor(private connection: DBConnectionImpl) {}
constructor(private connection: DbConnectionImpl) {}
get hasSpecialStuff(): HasSpecialStuffTableHandle {
return new HasSpecialStuffTableHandle(this.connection.clientCache.getOrCreateTable<HasSpecialStuff>(REMOTE_MODULE.tables.has_special_stuff));
@@ -1327,8 +1327,8 @@ export class RemoteTables {
export class SubscriptionBuilder extends SubscriptionBuilderImpl<RemoteTables, RemoteReducers, SetReducerFlags> { }
export class DBConnection extends DBConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {
static builder = (): DBConnectionBuilder<DBConnection, ErrorContext, SubscriptionEventContext> => {
export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {
static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {
return new DBConnectionBuilder<DBConnection, ErrorContext, SubscriptionEventContext>(REMOTE_MODULE, (imp: DBConnectionImpl) => imp as DBConnection);
}
subscriptionBuilder = (): SubscriptionBuilder => {
@@ -1355,9 +1355,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1418,9 +1418,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1478,9 +1478,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1630,9 +1630,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1703,9 +1703,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1779,9 +1779,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1887,9 +1887,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -1954,9 +1954,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2084,9 +2084,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2149,9 +2149,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2301,9 +2301,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2368,9 +2368,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2433,9 +2433,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2511,9 +2511,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2589,9 +2589,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2652,9 +2652,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2712,9 +2712,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2820,9 +2820,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2887,9 +2887,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -2952,9 +2952,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3012,9 +3012,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3090,9 +3090,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3157,9 +3157,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3220,9 +3220,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3280,9 +3280,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3360,9 +3360,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3425,9 +3425,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3533,9 +3533,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3598,9 +3598,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3678,9 +3678,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
@@ -3743,9 +3743,9 @@ import {
BinaryWriter,
CallReducerFlags,
ConnectionId,
DBConnectionBuilder,
DBConnectionImpl,
DBContext,
DbConnectionBuilder,
DbConnectionImpl,
DbContext,
ErrorContextInterface,
Event,
EventContextInterface,
+1
View File
@@ -18,6 +18,7 @@ enum-as-inner.workspace = true
flate2.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_with.workspace = true
smallvec.workspace = true
strum.workspace = true
thiserror.workspace = true
+110 -12
View File
@@ -50,7 +50,7 @@ pub enum PublishResult {
/// In other words, this echoes back a domain name if one was given. If
/// the database name given was in fact a database identity, this will be
/// `None`.
domain: Option<String>,
domain: Option<DatabaseName>,
/// The identity of the published database.
///
/// Always set, regardless of whether publish resolved a domain name first
@@ -59,14 +59,6 @@ pub enum PublishResult {
op: PublishOp,
},
// TODO: below variants are obsolete with control db module
/// The top level domain for the database name is not registered. For example:
///
/// - `clockworklabs/bitcraft`
///
/// if `clockworklabs` is not registered, this error is returned.
TldNotRegistered { domain: DomainName },
/// The top level domain for the database name is registered, but the identity that you provided does
/// not have permission to insert the given database name. For example:
///
@@ -75,7 +67,7 @@ pub enum PublishResult {
/// If you were trying to insert this database name, but the tld `clockworklabs` is
/// owned by an identity other than the identity that you provided, then you will receive
/// this error.
PermissionDenied { domain: DomainName },
PermissionDenied { name: DatabaseName },
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -118,6 +110,106 @@ pub enum SetDefaultDomainResult {
},
}
/// A simplified version of [`DomainName`] that allows a limited set of characters.
///
/// Must match the regex `^[a-z0-9]+(-[a-z0-9]+)*$`
#[derive(Clone, Debug, serde_with::DeserializeFromStr, serde_with::SerializeDisplay)]
pub struct DatabaseName(String);
impl AsRef<str> for DatabaseName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<DatabaseName> for String {
fn from(name: DatabaseName) -> Self {
name.0
}
}
impl fmt::Display for DatabaseName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(thiserror::Error, Clone, Copy, Debug)]
pub enum DatabaseNameError {
#[error("database names cannot be identities")]
Identity,
#[error("database names cannot be empty")]
Empty,
#[error("invalid hyphen in database name")]
Hyphen,
#[error("invalid characters in database name")]
Invalid,
}
fn parse_database_name(s: &str) -> Result<&str, DatabaseNameError> {
use DatabaseNameError::*;
if is_identity(s) {
return Err(Identity);
}
let mut chrs = s.chars();
let mut next = || chrs.next();
let is_az09 = |c: char| matches!(c, 'a'..='z' | '0'..='9');
let c = next().ok_or(Empty)?;
if c == '-' {
return Err(Hyphen);
} else if !is_az09(c) {
return Err(Invalid);
}
while let Some(c) = next() {
if c == '-' {
// can't have a hyphen at the end
let c = next().ok_or(Hyphen)?;
// can't have 2 hyphens in a row
if !is_az09(c) {
return Err(Hyphen);
}
} else if !is_az09(c) {
return Err(Invalid);
}
}
Ok(s)
}
impl TryFrom<String> for DatabaseName {
type Error = DatabaseNameError;
fn try_from(s: String) -> Result<Self, Self::Error> {
parse_database_name(&s)?;
Ok(Self(s))
}
}
impl FromStr for DatabaseName {
type Err = DatabaseNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(parse_database_name(s)?.to_owned()))
}
}
impl From<DatabaseName> for Tld {
fn from(name: DatabaseName) -> Self {
Tld(name.0)
}
}
impl From<DatabaseName> for DomainName {
fn from(name: DatabaseName) -> Self {
Tld::from(name).into()
}
}
/// The top level domain part of a [`DomainName`].
///
/// This newtype witnesses that the TLD is well-formed as per the parsing rules
@@ -317,6 +409,12 @@ impl AsRef<str> for DomainName {
}
}
impl From<DomainName> for String {
fn from(name: DomainName) -> Self {
name.domain_name
}
}
impl fmt::Display for DomainName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.domain_name)
@@ -420,8 +518,8 @@ mod serde_impls {
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ReverseDNSResponse {
pub names: Vec<DomainName>,
pub struct GetNamesResponse {
pub names: Vec<DatabaseName>,
}
/// Returns whether a hex string is a valid identity.
+3
View File
@@ -34,6 +34,9 @@ http.workspace = true
headers.workspace = true
mime = "0.3.17"
tokio-stream = { version = "0.1.12", features = ["sync"] }
tower-layer.workspace = true
tower-service.workspace = true
tower-http.workspace = true
futures = "0.3"
bytes = "1"
tracing.workspace = true
+2 -2
View File
@@ -185,7 +185,7 @@ pub trait ControlStateReadAccess {
fn get_energy_balance(&self, identity: &Identity) -> anyhow::Result<Option<EnergyBalance>>;
// DNS
fn lookup_identity(&self, domain: &DomainName) -> anyhow::Result<Option<Identity>>;
fn lookup_identity(&self, domain: &str) -> anyhow::Result<Option<Identity>>;
fn reverse_lookup(&self, database_identity: &Identity) -> anyhow::Result<Vec<DomainName>>;
}
@@ -259,7 +259,7 @@ impl<T: ControlStateReadAccess + ?Sized> ControlStateReadAccess for Arc<T> {
}
// DNS
fn lookup_identity(&self, domain: &DomainName) -> anyhow::Result<Option<Identity>> {
fn lookup_identity(&self, domain: &str) -> anyhow::Result<Option<Identity>> {
(**self).lookup_identity(domain)
}
+145 -129
View File
@@ -1,13 +1,14 @@
use crate::auth::{
anon_auth_middleware, SpacetimeAuth, SpacetimeAuthHeader, SpacetimeEnergyUsed, SpacetimeExecutionDurationMicros,
SpacetimeIdentity, SpacetimeIdentityToken,
anon_auth_middleware, SpacetimeAuth, SpacetimeEnergyUsed, SpacetimeExecutionDurationMicros, SpacetimeIdentity,
SpacetimeIdentityToken,
};
use crate::routes::subscribe::generate_random_connection_id;
use crate::util::{ByteStringBody, NameOrIdentity};
use crate::{log_and_500, ControlStateDelegate, DatabaseDef, NodeDelegate};
use axum::body::{Body, Bytes};
use axum::extract::{DefaultBodyLimit, Path, Query, State};
use axum::extract::{Path, Query, State};
use axum::response::{ErrorResponse, IntoResponse};
use axum::routing::MethodRouter;
use axum::Extension;
use axum_extra::TypedHeader;
use futures::StreamExt;
@@ -20,20 +21,12 @@ use spacetimedb::host::ReducerOutcome;
use spacetimedb::host::UpdateDatabaseResult;
use spacetimedb::identity::Identity;
use spacetimedb::messages::control_db::{Database, HostType};
use spacetimedb_client_api_messages::name::{self, DnsLookupResponse, DomainName, PublishOp, PublishResult};
use spacetimedb_client_api_messages::name::{self, DatabaseName, PublishOp, PublishResult};
use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9;
use spacetimedb_lib::identity::AuthCtx;
use spacetimedb_lib::sats;
use super::identity::IdentityForUrl;
pub(crate) struct DomainParsingRejection;
impl IntoResponse for DomainParsingRejection {
fn into_response(self) -> axum::response::Response {
(StatusCode::BAD_REQUEST, "Unable to parse domain name").into_response()
}
}
use super::subscribe::handle_websocket;
#[derive(Deserialize)]
pub struct CallParams {
@@ -48,13 +41,17 @@ pub async fn call<S: ControlStateDelegate + NodeDelegate>(
name_or_identity,
reducer,
}): Path<CallParams>,
TypedHeader(content_type): TypedHeader<headers::ContentType>,
ByteStringBody(body): ByteStringBody,
) -> axum::response::Result<impl IntoResponse> {
if content_type != headers::ContentType::json() {
return Err(axum::extract::rejection::MissingJsonContentType::default().into());
}
let caller_identity = auth.identity;
let args = ReducerArgs::Json(body);
let db_identity = name_or_identity.resolve(&worker_ctx).await?.into();
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
let database = worker_ctx_find_database(&worker_ctx, &db_identity)
.await?
.ok_or_else(|| {
@@ -180,7 +177,7 @@ pub async fn schema<S>(
where
S: ControlStateDelegate + NodeDelegate,
{
let db_identity = name_or_identity.resolve(&worker_ctx).await?.into();
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
let database = worker_ctx_find_database(&worker_ctx, &db_identity)
.await?
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
@@ -236,7 +233,7 @@ pub async fn db_info<S: ControlStateDelegate>(
Path(DatabaseParam { name_or_identity }): Path<DatabaseParam>,
) -> axum::response::Result<impl IntoResponse> {
log::trace!("Trying to resolve database identity: {:?}", name_or_identity);
let database_identity = name_or_identity.resolve(&worker_ctx).await?.into();
let database_identity = name_or_identity.resolve(&worker_ctx).await?;
log::trace!("Resolved identity to: {database_identity:?}");
let database = worker_ctx_find_database(&worker_ctx, &database_identity)
.await?
@@ -271,7 +268,7 @@ where
// You should not be able to read the logs from a database that you do not own
// so, unless you are the owner, this will fail.
let database_identity: Identity = name_or_identity.resolve(&worker_ctx).await?.into();
let database_identity: Identity = name_or_identity.resolve(&worker_ctx).await?;
let database = worker_ctx_find_database(&worker_ctx, &database_identity)
.await?
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
@@ -373,7 +370,7 @@ where
// Anyone is authorized to execute SQL queries. The SQL engine will determine
// which queries this identity is allowed to execute against the database.
let db_identity = name_or_identity.resolve(&worker_ctx).await?.into();
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
let database = worker_ctx_find_database(&worker_ctx, &db_identity)
.await?
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
@@ -393,109 +390,97 @@ where
#[derive(Deserialize)]
pub struct DNSParams {
database_name: String,
name_or_identity: NameOrIdentity,
}
#[derive(Deserialize)]
pub struct ReverseDNSParams {
database_identity: IdentityForUrl,
name_or_identity: NameOrIdentity,
}
#[derive(Deserialize)]
pub struct DNSQueryParams {}
pub async fn dns<S: ControlStateDelegate>(
pub async fn get_identity<S: ControlStateDelegate>(
State(ctx): State<S>,
Path(DNSParams { database_name }): Path<DNSParams>,
Path(DNSParams { name_or_identity }): Path<DNSParams>,
Query(DNSQueryParams {}): Query<DNSQueryParams>,
) -> axum::response::Result<impl IntoResponse> {
let domain = database_name.parse().map_err(|_| DomainParsingRejection)?;
let db_identity = ctx.lookup_identity(&domain).map_err(log_and_500)?;
let response = if let Some(db_identity) = db_identity {
DnsLookupResponse::Success {
domain,
identity: db_identity,
}
} else {
DnsLookupResponse::Failure { domain }
};
Ok(axum::Json(response))
let identity = name_or_identity.resolve(&ctx).await?;
Ok(identity.to_string())
}
pub async fn reverse_dns<S: ControlStateDelegate>(
pub async fn get_names<S: ControlStateDelegate>(
State(ctx): State<S>,
Path(ReverseDNSParams { database_identity }): Path<ReverseDNSParams>,
Path(ReverseDNSParams { name_or_identity }): Path<ReverseDNSParams>,
) -> axum::response::Result<impl IntoResponse> {
let database_identity = Identity::from(database_identity);
let database_identity = name_or_identity.resolve(&ctx).await?;
let names = ctx.reverse_lookup(&database_identity).map_err(log_and_500)?;
let names = ctx
.reverse_lookup(&database_identity)
.map_err(log_and_500)?
.into_iter()
.filter_map(|x| String::from(x).try_into().ok())
.collect();
let response = name::ReverseDNSResponse { names };
let response = name::GetNamesResponse { names };
Ok(axum::Json(response))
}
#[derive(Deserialize)]
pub struct RegisterTldParams {
tld: String,
pub struct PublishDatabaseParams {
name_or_identity: Option<NameOrIdentity>,
}
pub async fn register_tld<S: ControlStateDelegate>(
State(ctx): State<S>,
Query(RegisterTldParams { tld }): Query<RegisterTldParams>,
Extension(auth): Extension<SpacetimeAuth>,
) -> axum::response::Result<impl IntoResponse> {
// You should not be able to publish to a database that you do not own
// so, unless you are the owner, this will fail, hence not using get_or_create
let tld = tld.parse::<DomainName>().map_err(|_| DomainParsingRejection)?.into();
let result = ctx.register_tld(&auth.identity, tld).await.map_err(log_and_500)?;
Ok(axum::Json(result))
}
#[derive(Deserialize)]
pub struct PublishDatabaseParams {}
#[derive(Deserialize)]
pub struct PublishDatabaseQueryParams {
#[serde(default)]
clear: bool,
name_or_identity: Option<NameOrIdentity>,
}
impl PublishDatabaseQueryParams {
pub fn name_or_identity(&self) -> Option<&NameOrIdentity> {
self.name_or_identity.as_ref()
}
}
pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
State(ctx): State<S>,
Path(PublishDatabaseParams {}): Path<PublishDatabaseParams>,
Query(query_params): Query<PublishDatabaseQueryParams>,
Path(PublishDatabaseParams { name_or_identity }): Path<PublishDatabaseParams>,
Query(PublishDatabaseQueryParams { clear }): Query<PublishDatabaseQueryParams>,
Extension(auth): Extension<SpacetimeAuth>,
body: Bytes,
) -> axum::response::Result<axum::Json<PublishResult>> {
let PublishDatabaseQueryParams {
name_or_identity,
clear,
} = query_params;
// You should not be able to publish to a database that you do not own
// so, unless you are the owner, this will fail.
let (database_identity, db_name) = match name_or_identity {
let (database_identity, db_name) = match &name_or_identity {
Some(noa) => match noa.try_resolve(&ctx).await? {
Ok(resolved) => resolved.into(),
Err(domain) => {
Ok(resolved) => (resolved, noa.name()),
Err(name) => {
// `name_or_identity` was a `NameOrIdentity::Name`, but no record
// exists yet. Create it now with a fresh identity.
let database_auth = SpacetimeAuth::alloc(&ctx).await?;
let database_identity = database_auth.identity;
ctx.create_dns_record(&auth.identity, &domain, &database_identity)
let tld: name::Tld = name.clone().into();
let tld = match ctx.register_tld(&auth.identity, tld).await.map_err(log_and_500)? {
name::RegisterTldResult::Success { domain }
| name::RegisterTldResult::AlreadyRegistered { domain } => domain,
name::RegisterTldResult::Unauthorized { .. } => {
return Err((
StatusCode::UNAUTHORIZED,
axum::Json(PublishResult::PermissionDenied { name: name.clone() }),
)
.into())
}
};
let res = ctx
.create_dns_record(&auth.identity, &tld.into(), &database_identity)
.await
.map_err(log_and_500)?;
(database_identity, Some(domain))
match res {
name::InsertDomainResult::Success { .. } => {}
name::InsertDomainResult::TldNotRegistered { .. }
| name::InsertDomainResult::PermissionDenied { .. } => {
return Err(log_and_500("impossible: we just registered the tld"))
}
name::InsertDomainResult::OtherError(e) => return Err(log_and_500(e)),
}
(database_identity, Some(name))
}
},
None => {
@@ -556,7 +541,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
}
Ok(axum::Json(PublishResult::Success {
domain: db_name.as_ref().map(ToString::to_string),
domain: db_name.cloned(),
database_identity,
op,
}))
@@ -564,15 +549,15 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
#[derive(Deserialize)]
pub struct DeleteDatabaseParams {
database_identity: IdentityForUrl,
name_or_identity: NameOrIdentity,
}
pub async fn delete_database<S: ControlStateDelegate>(
State(ctx): State<S>,
Path(DeleteDatabaseParams { database_identity }): Path<DeleteDatabaseParams>,
Path(DeleteDatabaseParams { name_or_identity }): Path<DeleteDatabaseParams>,
Extension(auth): Extension<SpacetimeAuth>,
) -> axum::response::Result<impl IntoResponse> {
let database_identity = Identity::from(database_identity);
let database_identity = name_or_identity.resolve(&ctx).await?;
ctx.delete_database(&auth.identity, &database_identity)
.await
@@ -582,76 +567,107 @@ pub async fn delete_database<S: ControlStateDelegate>(
}
#[derive(Deserialize)]
pub struct SetNameQueryParams {
domain: String,
database_identity: IdentityForUrl,
pub struct AddNameParams {
name_or_identity: NameOrIdentity,
}
pub async fn set_name<S: ControlStateDelegate>(
pub async fn add_name<S: ControlStateDelegate>(
State(ctx): State<S>,
Query(SetNameQueryParams {
domain,
database_identity,
}): Query<SetNameQueryParams>,
Path(AddNameParams { name_or_identity }): Path<AddNameParams>,
Extension(auth): Extension<SpacetimeAuth>,
name: String,
) -> axum::response::Result<impl IntoResponse> {
let database_identity = Identity::from(database_identity);
let name = DatabaseName::try_from(name).map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;
let database_identity = name_or_identity.resolve(&ctx).await?;
let database = ctx
.get_database_by_identity(&database_identity)
.map_err(log_and_500)?
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
if database.owner_identity != auth.identity {
return Err((StatusCode::UNAUTHORIZED, "Identity does not own database.").into());
}
let domain = domain.parse().map_err(|_| DomainParsingRejection)?;
let response = ctx
.create_dns_record(&auth.identity, &domain, &database_identity)
.create_dns_record(&auth.identity, &name.into(), &database_identity)
.await
// TODO: better error code handling
.map_err(log_and_500)?;
Ok(axum::Json(response))
let code = match response {
name::InsertDomainResult::Success { .. } => StatusCode::OK,
name::InsertDomainResult::TldNotRegistered { .. } => StatusCode::BAD_REQUEST,
name::InsertDomainResult::PermissionDenied { .. } => StatusCode::UNAUTHORIZED,
name::InsertDomainResult::OtherError(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
Ok((code, axum::Json(response)))
}
/// This API call is just designed to allow clients to determine whether or not they can
/// establish a connection to SpacetimeDB. This API call doesn't actually do anything.
pub async fn ping<S>(State(_ctx): State<S>, _auth: SpacetimeAuthHeader) -> axum::response::Result<impl IntoResponse> {
Ok(())
/// This struct allows the edition to customize `/database` routes more meticulously.
pub struct DatabaseRoutes<S> {
/// POST /database
pub root_post: MethodRouter<S>,
/// PUT: /database/:name_or_identity
pub db_put: MethodRouter<S>,
/// GET: /database/:name_or_identity
pub db_get: MethodRouter<S>,
/// DELETE: /database/:name_or_identity
pub db_delete: MethodRouter<S>,
/// GET: /database/:name_or_identity/names
pub names_get: MethodRouter<S>,
/// POST: /database/:name_or_identity/names
pub names_post: MethodRouter<S>,
/// GET: /database/:name_or_identity/identity
pub identity_get: MethodRouter<S>,
/// GET: /database/:name_or_identity/subscribe
pub subscribe_get: MethodRouter<S>,
/// POST: /database/:name_or_identity/call/:reducer
pub call_reducer_post: MethodRouter<S>,
/// GET: /database/:name_or_identity/schema
pub schema_get: MethodRouter<S>,
/// GET: /database/:name_or_identity/logs
pub logs_get: MethodRouter<S>,
/// POST: /database/:name_or_identity/sql
pub sql_post: MethodRouter<S>,
}
pub fn control_routes<S>(ctx: S) -> axum::Router<S>
impl<S> Default for DatabaseRoutes<S>
where
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
{
use axum::routing::{get, post};
axum::Router::new()
.route("/dns/:database_name", get(dns::<S>))
.route("/reverse_dns/:database_identity", get(reverse_dns::<S>))
.route("/set_name", get(set_name::<S>))
.route("/ping", get(ping::<S>))
.route("/register_tld", get(register_tld::<S>))
.route("/publish", post(publish::<S>).layer(DefaultBodyLimit::disable()))
.route("/delete/:database_identity", post(delete_database::<S>))
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
fn default() -> Self {
use axum::routing::{delete, get, post, put};
Self {
root_post: post(publish::<S>),
db_put: put(publish::<S>),
db_get: get(db_info::<S>),
db_delete: delete(delete_database::<S>),
names_get: get(get_names::<S>),
names_post: post(add_name::<S>),
identity_get: get(get_identity::<S>),
subscribe_get: get(handle_websocket::<S>),
call_reducer_post: post(call::<S>),
schema_get: get(schema::<S>),
logs_get: get(logs::<S>),
sql_post: post(sql::<S>),
}
}
}
pub fn worker_routes<S>(ctx: S) -> axum::Router<S>
impl<S> DatabaseRoutes<S>
where
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
{
use axum::routing::{get, post};
axum::Router::new()
.route("/:name_or_identity", get(db_info::<S>))
.route(
"/subscribe/:name_or_identity",
get(super::subscribe::handle_websocket::<S>),
)
.route("/call/:name_or_identity/:reducer", post(call::<S>))
.route("/schema/:name_or_identity", get(schema::<S>))
.route("/logs/:name_or_identity", get(logs::<S>))
.route("/sql/:name_or_identity", post(sql::<S>))
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
pub fn into_router(self, ctx: S) -> axum::Router<S> {
let db_router = axum::Router::<S>::new()
.route("/", self.db_put)
.route("/", self.db_get)
.route("/", self.db_delete)
.route("/names", self.names_get)
.route("/names", self.names_post)
.route("/identity", self.identity_get)
.route("/subscribe", self.subscribe_get)
.route("/call/:reducer", self.call_reducer_post)
.route("/schema", self.schema_get)
.route("/logs", self.logs_get)
.route("/sql", self.sql_post);
axum::Router::new()
.route("/", self.root_post)
.nest("/:name_or_identity", db_router)
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
}
}
+2 -3
View File
@@ -124,11 +124,10 @@ where
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
{
use axum::routing::get;
// TODO: rework this. probably no path param.
axum::Router::new().route(
"/:identity",
get(get_energy_balance::<S>)
.post(set_energy_balance::<S>)
.put(add_energy::<S>),
.put(set_energy_balance::<S>)
.post(add_energy::<S>),
)
}
+2 -3
View File
@@ -29,13 +29,12 @@ pub async fn health<S: ControlStateDelegate + NodeDelegate>(
.map(|n| n.unschedulable)
.unwrap_or(false);
Ok(serde_json::json!({
Ok(axum::Json(serde_json::json!({
"package_name": PACKAGE_NAME,
"version": VERSION,
"nodes": nodes,
"schedulable": schedulable,
})
.to_string())
})))
}
pub fn router<S>() -> axum::Router<S>
+2 -2
View File
@@ -135,7 +135,7 @@ pub async fn get_public_key<S: NodeDelegate>(State(ctx): State<S>) -> axum::resp
))
}
pub fn router<S>(_: S) -> axum::Router<S>
pub fn router<S>() -> axum::Router<S>
where
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
{
@@ -143,7 +143,7 @@ where
axum::Router::new()
.route("/", post(create_identity::<S>))
.route("/public-key", get(get_public_key::<S>))
.route("/websocket_token", post(create_websocket_token::<S>))
.route("/websocket-token", post(create_websocket_token::<S>))
.route("/:identity/verify", get(validate_token))
.route("/:identity/databases", get(get_databases::<S>))
}
+33
View File
@@ -1,3 +1,9 @@
use database::DatabaseRoutes;
use http::header;
use tower_http::cors;
use crate::{ControlStateDelegate, NodeDelegate};
pub mod database;
pub mod energy;
pub mod health;
@@ -5,3 +11,30 @@ pub mod identity;
pub mod metrics;
pub mod prometheus;
pub mod subscribe;
/// This API call is just designed to allow clients to determine whether or not they can
/// establish a connection to SpacetimeDB. This API call doesn't actually do anything.
pub async fn ping(_auth: crate::auth::SpacetimeAuthHeader) {}
#[allow(clippy::let_and_return)]
pub fn router<S>(ctx: &S, database_routes: DatabaseRoutes<S>, extra: axum::Router<S>) -> axum::Router<S>
where
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
{
use axum::routing::get;
let router = axum::Router::new()
.nest("/database", database_routes.into_router(ctx.clone()))
.nest("/identity", identity::router())
.nest("/energy", energy::router())
.nest("/prometheus", prometheus::router())
.nest("/metrics", metrics::router())
.route("/ping", get(ping))
.merge(extra);
let cors = cors::CorsLayer::new()
.allow_headers([header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE])
.allow_methods(cors::Any)
.allow_origin(cors::Any);
axum::Router::new().nest("/v1", router.layer(cors))
}
+1 -1
View File
@@ -85,7 +85,7 @@ where
))?;
}
let db_identity = name_or_identity.resolve(&ctx).await?.into();
let db_identity = name_or_identity.resolve(&ctx).await?;
let (res, ws_upgrade, protocol) =
ws.select_protocol([(BIN_PROTOCOL, Protocol::Binary), (TEXT_PROTOCOL, Protocol::Text)]);
+47 -63
View File
@@ -8,12 +8,13 @@ use axum::body::Bytes;
use axum::extract::{FromRequest, Request};
use axum::response::IntoResponse;
use bytestring::ByteString;
use futures::TryStreamExt;
use http::{HeaderName, HeaderValue, StatusCode};
use hyper::body::Body;
use spacetimedb::Identity;
use spacetimedb_client_api_messages::name::DomainName;
use spacetimedb_client_api_messages::name::DatabaseName;
use crate::routes::database::DomainParsingRejection;
use crate::routes::identity::IdentityForUrl;
use crate::{log_and_500, ControlStateReadAccess};
@@ -60,62 +61,52 @@ impl headers::Header for XForwardedFor {
#[derive(Clone, Debug)]
pub enum NameOrIdentity {
Identity(IdentityForUrl),
Name(String),
Name(DatabaseName),
}
impl NameOrIdentity {
pub fn into_string(self) -> String {
match self {
NameOrIdentity::Identity(addr) => Identity::from(addr).to_hex().to_string(),
NameOrIdentity::Name(name) => name,
NameOrIdentity::Name(name) => name.into(),
}
}
pub fn name(&self) -> Option<&DatabaseName> {
if let Self::Name(name) = self {
Some(name)
} else {
None
}
}
/// Resolve this [`NameOrIdentity`].
///
/// If `self` is a [`NameOrIdentity::Identity`], the inner [`Identity`] is
/// returned in a [`ResolvedIdentity`] without a [`DomainName`].
/// returned directly.
///
/// Otherwise, if `self` is a [`NameOrIdentity::Name`], the [`Identity`] is
/// looked up by that name in the SpacetimeDB DNS and returned in a
/// [`ResolvedIdentity`] alongside `Some` [`DomainName`].
/// looked up by that name in the SpacetimeDB DNS and returned.
///
/// Errors are returned if [`NameOrIdentity::Name`] cannot be parsed into a
/// [`DomainName`], or the DNS lookup fails.
/// Errors are returned if [`NameOrIdentity::Name`] the DNS lookup fails.
///
/// An `Ok` result is itself a [`Result`], which is `Err(DomainName)` if the
/// An `Ok` result is itself a [`Result`], which is `Err(DatabaseName)` if the
/// given [`NameOrIdentity::Name`] is not registered in the SpacetimeDB DNS,
/// i.e. no corresponding [`Identity`] exists.
pub async fn try_resolve(
&self,
ctx: &(impl ControlStateReadAccess + ?Sized),
) -> axum::response::Result<Result<ResolvedIdentity, DomainName>> {
) -> axum::response::Result<Result<Identity, &DatabaseName>> {
Ok(match self {
Self::Identity(identity) => Ok(ResolvedIdentity {
identity: Identity::from(*identity),
domain: None,
}),
Self::Name(name) => {
let domain = name.parse().map_err(|_| DomainParsingRejection)?;
let identity = ctx.lookup_identity(&domain).map_err(log_and_500)?;
match identity {
Some(identity) => Ok(ResolvedIdentity {
identity,
domain: Some(domain),
}),
None => Err(domain),
}
}
Self::Identity(identity) => Ok(Identity::from(*identity)),
Self::Name(name) => ctx.lookup_identity(name.as_ref()).map_err(log_and_500)?.ok_or(name),
})
}
/// A variant of [`Self::try_resolve()`] which maps to a 404 (Not Found)
/// response if `self` is a [`NameOrIdentity::Name`] for which no
/// corresponding [`Identity`] is found in the SpacetimeDB DNS.
pub async fn resolve(
&self,
ctx: &(impl ControlStateReadAccess + ?Sized),
) -> axum::response::Result<ResolvedIdentity> {
pub async fn resolve(&self, ctx: &(impl ControlStateReadAccess + ?Sized)) -> axum::response::Result<Identity> {
self.try_resolve(ctx).await?.map_err(|_| StatusCode::NOT_FOUND.into())
}
}
@@ -125,13 +116,13 @@ impl<'de> serde::Deserialize<'de> for NameOrIdentity {
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer).map(|s| {
if let Ok(addr) = Identity::from_hex(&s) {
NameOrIdentity::Identity(IdentityForUrl::from(addr))
} else {
NameOrIdentity::Name(s)
}
})
let s = String::deserialize(deserializer)?;
if let Ok(addr) = Identity::from_hex(&s) {
Ok(NameOrIdentity::Identity(IdentityForUrl::from(addr)))
} else {
let name: DatabaseName = s.try_into().map_err(serde::de::Error::custom)?;
Ok(NameOrIdentity::Name(name))
}
}
}
@@ -139,37 +130,30 @@ impl fmt::Display for NameOrIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Identity(addr) => f.write_str(addr.into_inner().to_hex().as_str()),
Self::Name(name) => f.write_str(name),
Self::Name(name) => f.write_str(name.as_ref()),
}
}
}
/// A resolved [`NameOrIdentity`].
///
/// Constructed by [`NameOrIdentity::try_resolve()`].
pub struct ResolvedIdentity {
identity: Identity,
domain: Option<DomainName>,
}
pub struct EmptyBody;
impl ResolvedIdentity {
pub fn identity(&self) -> &Identity {
&self.identity
}
#[async_trait::async_trait]
impl<S> FromRequest<S> for EmptyBody {
type Rejection = axum::response::Response;
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let body = req.into_body();
if body.is_end_stream() {
return Ok(Self);
}
pub fn domain(&self) -> Option<&DomainName> {
self.domain.as_ref()
}
}
impl From<ResolvedIdentity> for Identity {
fn from(value: ResolvedIdentity) -> Self {
value.identity
}
}
impl From<ResolvedIdentity> for (Identity, Option<DomainName>) {
fn from(ResolvedIdentity { identity, domain }: ResolvedIdentity) -> Self {
(identity, domain)
if body
.into_data_stream()
.try_any(|data| futures::future::ready(!data.is_empty()))
.await
.map_err(|_| (StatusCode::BAD_REQUEST, "Failed to buffer the request body").into_response())?
{
return Err((StatusCode::BAD_REQUEST, "body must be empty").into_response());
}
Ok(Self)
}
}
+2 -1
View File
@@ -123,8 +123,9 @@ fn make_uri(host: Uri, db_name: &str, connection_id: ConnectionId, params: WsPar
path.push('/');
}
path.push_str("database/subscribe/");
path.push_str("v1/database/");
path.push_str(db_name);
path.push_str("/subscribe");
// Provide the connection ID.
path.push_str("?connection_id=");
+33 -33
View File
@@ -69,52 +69,52 @@ fn main() {
.expect("Pass a test name as a command-line argument to the test client");
match &*test {
"insert_primitive" => exec_insert_primitive(),
"subscribe_and_cancel" => exec_subscribe_and_cancel(),
"subscribe_and_unsubscribe" => exec_subscribe_and_unsubscribe(),
"subscription_error_smoke_test" => exec_subscription_error_smoke_test(),
"delete_primitive" => exec_delete_primitive(),
"update_primitive" => exec_update_primitive(),
"insert-primitive" => exec_insert_primitive(),
"subscribe-and-cancel" => exec_subscribe_and_cancel(),
"subscribe-and-unsubscribe" => exec_subscribe_and_unsubscribe(),
"subscription-error-smoke-test" => exec_subscription_error_smoke_test(),
"delete-primitive" => exec_delete_primitive(),
"update-primitive" => exec_update_primitive(),
"insert_identity" => exec_insert_identity(),
"insert_caller_identity" => exec_insert_caller_identity(),
"delete_identity" => exec_delete_identity(),
"update_identity" => exec_update_identity(),
"insert-identity" => exec_insert_identity(),
"insert-caller-identity" => exec_insert_caller_identity(),
"delete-identity" => exec_delete_identity(),
"update-identity" => exec_update_identity(),
"insert_connection_id" => exec_insert_connection_id(),
"insert_caller_connection_id" => exec_insert_caller_connection_id(),
"delete_connection_id" => exec_delete_connection_id(),
"update_connection_id" => exec_update_connection_id(),
"insert-connection-id" => exec_insert_connection_id(),
"insert-caller-connection-id" => exec_insert_caller_connection_id(),
"delete-connection-id" => exec_delete_connection_id(),
"update-connection-id" => exec_update_connection_id(),
"insert_timestamp" => exec_insert_timestamp(),
"insert_call_timestamp" => exec_insert_call_timestamp(),
"insert-timestamp" => exec_insert_timestamp(),
"insert-call-timestamp" => exec_insert_call_timestamp(),
"on_reducer" => exec_on_reducer(),
"fail_reducer" => exec_fail_reducer(),
"on-reducer" => exec_on_reducer(),
"fail-reducer" => exec_fail_reducer(),
"insert_vec" => exec_insert_vec(),
"insert_option_some" => exec_insert_option_some(),
"insert_option_none" => exec_insert_option_none(),
"insert_struct" => exec_insert_struct(),
"insert_simple_enum" => exec_insert_simple_enum(),
"insert_enum_with_payload" => exec_insert_enum_with_payload(),
"insert-vec" => exec_insert_vec(),
"insert-option-some" => exec_insert_option_some(),
"insert-option-none" => exec_insert_option_none(),
"insert-struct" => exec_insert_struct(),
"insert-simple-enum" => exec_insert_simple_enum(),
"insert-enum-with-payload" => exec_insert_enum_with_payload(),
"insert_delete_large_table" => exec_insert_delete_large_table(),
"insert-delete-large-table" => exec_insert_delete_large_table(),
"insert_primitives_as_strings" => exec_insert_primitives_as_strings(),
"insert-primitives-as-strings" => exec_insert_primitives_as_strings(),
// "resubscribe" => exec_resubscribe(),
//
"reauth_part_1" => exec_reauth_part_1(),
"reauth_part_2" => exec_reauth_part_2(),
"reauth-part-1" => exec_reauth_part_1(),
"reauth-part-2" => exec_reauth_part_2(),
"should_fail" => exec_should_fail(),
"should-fail" => exec_should_fail(),
"reconnect_same_connection_id" => exec_reconnect_same_connection_id(),
"caller_always_notified" => exec_caller_always_notified(),
"reconnect-same-connection-id" => exec_reconnect_same_connection_id(),
"caller-always-notified" => exec_caller_always_notified(),
"subscribe_all_select_star" => exec_subscribe_all_select_star(),
"caller_alice_receives_reducer_callback_but_not_bob" => {
"subscribe-all-select-star" => exec_subscribe_all_select_star(),
"caller-alice-receives-reducer-callback-but-not-bob" => {
exec_caller_alice_receives_reducer_callback_but_not_bob()
}
_ => panic!("Unknown test: {}", test),
+34 -34
View File
@@ -20,131 +20,131 @@ macro_rules! declare_tests_with_suffix {
#[test]
fn insert_primitive() {
make_test("insert_primitive").run();
make_test("insert-primitive").run();
}
#[test]
fn subscribe_and_cancel() {
make_test("subscribe_and_cancel").run();
make_test("subscribe-and-cancel").run();
}
#[test]
fn subscribe_and_unsubscribe() {
make_test("subscribe_and_unsubscribe").run();
make_test("subscribe-and-unsubscribe").run();
}
#[test]
fn subscription_error_smoke_test() {
make_test("subscription_error_smoke_test").run();
make_test("subscription-error-smoke-test").run();
}
#[test]
fn delete_primitive() {
make_test("delete_primitive").run();
make_test("delete-primitive").run();
}
#[test]
fn update_primitive() {
make_test("update_primitive").run();
make_test("update-primitive").run();
}
#[test]
fn insert_identity() {
make_test("insert_identity").run();
make_test("insert-identity").run();
}
#[test]
fn insert_caller_identity() {
make_test("insert_caller_identity").run();
make_test("insert-caller-identity").run();
}
#[test]
fn delete_identity() {
make_test("delete_identity").run();
make_test("delete-identity").run();
}
#[test]
fn update_identity() {
make_test("delete_identity").run();
make_test("delete-identity").run();
}
#[test]
fn insert_connection_id() {
make_test("insert_connection_id").run();
make_test("insert-connection-id").run();
}
#[test]
fn insert_caller_connection_id() {
make_test("insert_caller_connection_id").run();
make_test("insert-caller-connection-id").run();
}
#[test]
fn delete_connection_id() {
make_test("delete_connection_id").run();
make_test("delete-connection-id").run();
}
#[test]
fn update_connection_id() {
make_test("delete_connection_id").run();
make_test("delete-connection-id").run();
}
#[test]
fn insert_timestamp() {
make_test("insert_timestamp").run();
make_test("insert-timestamp").run();
}
#[test]
fn insert_call_timestamp() {
make_test("insert_call_timestamp").run();
make_test("insert-call-timestamp").run();
}
#[test]
fn on_reducer() {
make_test("on_reducer").run();
make_test("on-reducer").run();
}
#[test]
fn fail_reducer() {
make_test("fail_reducer").run();
make_test("fail-reducer").run();
}
#[test]
fn insert_vec() {
make_test("insert_vec").run();
make_test("insert-vec").run();
}
#[test]
fn insert_option_some() {
make_test("insert_option_some").run();
make_test("insert-option-some").run();
}
#[test]
fn insert_option_none() {
make_test("insert_option_none").run();
make_test("insert-option-none").run();
}
#[test]
fn insert_struct() {
make_test("insert_struct").run();
make_test("insert-struct").run();
}
#[test]
fn insert_simple_enum() {
make_test("insert_simple_enum").run();
make_test("insert-simple-enum").run();
}
#[test]
fn insert_enum_with_payload() {
make_test("insert_enum_with_payload").run();
make_test("insert-enum-with-payload").run();
}
#[test]
fn insert_delete_large_table() {
make_test("insert_delete_large_table").run();
make_test("insert-delete-large-table").run();
}
#[test]
fn insert_primitives_as_strings() {
make_test("insert_primitives_as_strings").run();
make_test("insert-primitives-as-strings").run();
}
// #[test]
@@ -155,24 +155,24 @@ macro_rules! declare_tests_with_suffix {
#[test]
#[should_panic]
fn should_fail() {
make_test("should_fail").run();
make_test("should-fail").run();
}
#[test]
fn reauth() {
make_test("reauth_part_1").run();
make_test("reauth_part_2").run();
make_test("reauth-part-1").run();
make_test("reauth-part-2").run();
}
#[test]
fn reconnect_same_connection_id() {
make_test("reconnect_same_connection_id").run();
make_test("reconnect-same-connection-id").run();
}
#[test]
fn connect_disconnect_callbacks() {
Test::builder()
.with_name(concat!("connect_disconnect_callback_", stringify!($lang)))
.with_name(concat!("connect-disconnect-callback-", stringify!($lang)))
.with_module(concat!("sdk-test-connect-disconnect", $suffix))
.with_client(concat!(
env!("CARGO_MANIFEST_DIR"),
@@ -188,17 +188,17 @@ macro_rules! declare_tests_with_suffix {
#[test]
fn caller_always_notified() {
make_test("caller_always_notified").run();
make_test("caller-always-notified").run();
}
#[test]
fn subscribe_all_select_star() {
make_test("subscribe_all_select_star").run();
make_test("subscribe-all-select-star").run();
}
#[test]
fn caller_alice_receives_reducer_callback_but_not_bob() {
make_test("caller_alice_receives_reducer_callback_but_not_bob").run();
make_test("caller-alice-receives-reducer-callback-but-not-bob").run();
}
}
};
+2 -2
View File
@@ -78,7 +78,7 @@ impl ControlDb {
}
impl ControlDb {
pub fn spacetime_dns(&self, domain: &DomainName) -> Result<Option<Identity>> {
pub fn spacetime_dns(&self, domain: &str) -> Result<Option<Identity>> {
let tree = self.db.open_tree("dns")?;
let value = tree.get(domain.to_lowercase().as_bytes())?;
if let Some(value) = value {
@@ -118,7 +118,7 @@ impl ControlDb {
try_register_tld: bool,
) -> Result<InsertDomainResult> {
let database_identity = *database_identity;
if self.spacetime_dns(&domain)?.is_some() {
if self.spacetime_dns(domain.as_ref())?.is_some() {
return Err(Error::RecordAlreadyExists(domain));
}
let tld = domain.tld();
+2 -2
View File
@@ -67,11 +67,11 @@ fn test_domain() -> anyhow::Result<()> {
let tld_owner = cdb.spacetime_lookup_tld(domain.tld())?;
assert_eq!(tld_owner, Some(*ALICE));
let registered_addr = cdb.spacetime_dns(&domain)?;
let registered_addr = cdb.spacetime_dns(domain.as_ref())?;
assert_eq!(registered_addr, Some(addr));
// Try lowercase, too
let registered_addr = cdb.spacetime_dns(&domain_lower)?;
let registered_addr = cdb.spacetime_dns(domain_lower.as_ref())?;
assert_eq!(registered_addr, Some(addr));
// Reverse should yield the original domain (in mixed-case)
+1 -2
View File
@@ -1,6 +1,5 @@
mod control_db;
mod energy_monitor;
pub mod routes;
pub mod subcommands;
pub mod util;
pub mod version;
@@ -208,7 +207,7 @@ impl spacetimedb_client_api::ControlStateReadAccess for StandaloneEnv {
}
// DNS
fn lookup_identity(&self, domain: &DomainName) -> anyhow::Result<Option<Identity>> {
fn lookup_identity(&self, domain: &str) -> anyhow::Result<Option<Identity>> {
Ok(self.control_db.spacetime_dns(domain)?)
}
+8 -2
View File
@@ -1,13 +1,15 @@
use std::sync::Arc;
use crate::routes::router;
use crate::StandaloneEnv;
use anyhow::Context;
use axum::extract::DefaultBodyLimit;
use clap::ArgAction::SetTrue;
use clap::{Arg, ArgMatches};
use spacetimedb::config::{CertificateAuthority, ConfigFile};
use spacetimedb::db::{Config, Storage};
use spacetimedb::startup::{self, TracingOptions};
use spacetimedb_client_api::routes::database::DatabaseRoutes;
use spacetimedb_client_api::routes::router;
use spacetimedb_paths::cli::{PrivKeyPath, PubKeyPath};
use spacetimedb_paths::server::ServerDataDir;
use tokio::net::TcpListener;
@@ -132,7 +134,11 @@ pub async fn exec(args: &ArgMatches) -> anyhow::Result<()> {
let data_dir = Arc::new(data_dir.clone());
let ctx = StandaloneEnv::init(db_config, &certs, data_dir).await?;
let service = router(ctx);
let mut db_routes = DatabaseRoutes::default();
db_routes.root_post = db_routes.root_post.layer(DefaultBodyLimit::disable());
db_routes.db_put = db_routes.db_put.layer(DefaultBodyLimit::disable());
let extra = axum::Router::new().nest("/health", spacetimedb_client_api::routes::health::router());
let service = router(&ctx, db_routes, extra).with_state(ctx);
let tcp = TcpListener::bind(listen_addr).await?;
socket2::SockRef::from(&tcp).set_nodelay(true)?;
+5 -2
View File
@@ -1,5 +1,5 @@
use duct::cmd;
use rand::distributions::{Alphanumeric, DistString};
use rand::seq::IteratorRandom;
use spacetimedb_data_structures::map::HashMap;
use spacetimedb_paths::{RootDir, SpacetimePaths};
use std::fs::create_dir_all;
@@ -136,7 +136,10 @@ fn status_ok_or_panic(output: std::process::Output, command: &str, test_name: &s
}
fn random_module_name() -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
let mut rng = rand::thread_rng();
std::iter::repeat_with(|| ('a'..='z').chain('0'..='9').choose(&mut rng).unwrap())
.take(16)
.collect()
}
macro_rules! memoized {
+1 -1
View File
@@ -33,7 +33,7 @@ services:
--jwt-pub-key-path=/etc/spacetimedb/id_ecdsa.pub
--jwt-priv-key-path=/etc/spacetimedb/id_ecdsa'
healthcheck:
test: curl -f http://localhost/database/ping || exit 1
test: curl -f http://localhost/ping || exit 1
privileged: true
environment:
SPACETIMEDB_FLAMEGRAPH_PATH: ../../../../flamegraphs/flamegraph.folded
+1 -1
View File
@@ -77,7 +77,7 @@ def requires_docker(item):
return unittest.skip("docker not available")(item)
def random_string(k=20):
return ''.join(random.choices(string.ascii_letters, k=k))
return ''.join(random.choices(string.ascii_lowercase, k=k))
def extract_fields(cmd_output, field_name):
"""
+1 -1
View File
@@ -1,5 +1,5 @@
default_server = "127.0.0.1:3000"
spacetimedb_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJoZXhfaWRlbnRpdHkiOiJjMjAwODIzOTA3M2M5MDgyNWQxZWY0MWVjNGJlYzg1MmNkNWIzOTdiMzBjZTVhZjUzNmZlZGExOTE3OWM5ZTJjIiwic3ViIjoiMzVmMjQ5ZTYtZjI0NC00ZDE1LWIzYmUtM2Q5NWZjMjA4MTFmIiwiaXNzIjoibG9jYWxob3N0IiwiYXVkIjpbInNwYWNldGltZWRiIl0sImlhdCI6MTczMjYzODI4NywiZXhwIjpudWxsfQ.oVIaYaH7w8ZiuowAflzKo4BrUeGk_1WqlaySMCYqIrkzB96SxVjCQuR0PYM8dOs7WhsiXvYH7dgVxbSbVV4PGg"
spacetimedb_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJoZXhfaWRlbnRpdHkiOiJjMjAwMTk4MDViYzU2ZDczMmUwN2ZjZjlkYWJjYTJkY2I3ZGIwMzMwYjQyOGJjMTI0Y2E0YmMzNWQxNTgzZTkxIiwic3ViIjoiMDMwYmM2NjAtMDdlZC00ZDg0LThkMTYtMDRmMTUzMTgzZjZhIiwiaXNzIjoibG9jYWxob3N0IiwiYXVkIjpbInNwYWNldGltZWRiIl0sImlhdCI6MTczODk1MjY3OCwiZXhwIjpudWxsfQ.TInoGjszYrkVw6vqk_vfj_zgirJcf2-8aNQZHr8e0dJRYK9sCMh2RAjJ_odTbgXRmEIaW416vOZjFGVzuUH6Sg"
[[server_configs]]
nickname = "localhost"
+2
View File
@@ -1,4 +1,5 @@
from .. import Smoketest, random_string
import unittest
class Domains(Smoketest):
AUTOPUBLISH = False
@@ -19,6 +20,7 @@ class Domains(Smoketest):
# Now we're essentially just testing that it *doesn't* throw an exception
self.spacetime("logs", rand_name)
@unittest.expectedFailure
def test_subdomain_behavior(self):
"""Test how we treat the / character in published names"""
+1 -1
View File
@@ -29,7 +29,7 @@ def ping():
tries += 1
try:
print(f"Ping Server at {host}")
urlopen(f"http://{host}/database/ping")
urlopen(f"http://{host}/v1/ping")
print("Server up")
break
except Exception: