diff --git a/Cargo.lock b/Cargo.lock index c0081b7dd..3ef4d1a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,9 +276,9 @@ dependencies = [ [[package]] name = "async-std" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" dependencies = [ "async-attributes", "async-channel 1.9.0", @@ -507,9 +507,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -517,7 +517,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1159,9 +1159,9 @@ checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1455,9 +1455,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" dependencies = [ "value-bag", ] @@ -2227,9 +2227,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "indexmap 2.5.0", "itoa", @@ -2522,9 +2522,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", @@ -2753,7 +2753,7 @@ dependencies = [ [[package]] name = "typedb-protocol" version = "0.0.0" -source = "git+https://github.com/typedb/typedb-protocol?tag=3.0.0#111f1a9ed8aac3360c0b5d16e68c1ecebe823137" +source = "git+https://github.com/typedb/typedb-protocol?rev=7df6a6bfbbbd29940558ae7940b2065e1e2ba0b1#7df6a6bfbbbd29940558ae7940b2065e1e2ba0b1" dependencies = [ "prost", "tonic", @@ -2818,9 +2818,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "getrandom 0.3.1", "rand 0.9.0", @@ -2996,6 +2996,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/c/Cargo.toml b/c/Cargo.toml index 98c6cb67c..d158a1f86 100644 --- a/c/Cargo.toml +++ b/c/Cargo.toml @@ -21,7 +21,7 @@ features = {} [dependencies.log] features = ["kv", "kv_unstable", "std", "value-bag"] - version = "0.4.26" + version = "0.4.27" default-features = false [dependencies.typedb-driver] diff --git a/dependencies/typedb/repositories.bzl b/dependencies/typedb/repositories.bzl index b05f1bb20..447debfad 100644 --- a/dependencies/typedb/repositories.bzl +++ b/dependencies/typedb/repositories.bzl @@ -18,22 +18,25 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") def typedb_dependencies(): + # TODO: Return typedb git_repository( name = "typedb_dependencies", - remote = "https://github.com/typedb/typedb-dependencies", - commit = "cf9c1707c7896d61ff97bbf60b1880852ad42353", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_dependencies + remote = "https://github.com/farost/typedb-dependencies", + commit = "033496f9ead5e0f7516a232dcc2b5e031415a36a", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_dependencies ) def typedb_protocol(): + # TODO: Return typedb git_repository( name = "typedb_protocol", - remote = "https://github.com/typedb/typedb-protocol", - tag = "3.0.0", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_protocol + remote = "https://github.com/farost/typedb-protocol", + commit = "7df6a6bfbbbd29940558ae7940b2065e1e2ba0b1", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_protocol ) def typedb_behaviour(): + # TODO: Return typedb git_repository( name = "typedb_behaviour", - remote = "https://github.com/typedb/typedb-behaviour", - commit = "a5ca738d691e7e7abec0a69e68f6b06310ac2168", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_behaviour + remote = "https://github.com/farost/typedb-behaviour", + commit = "54d190804025d71e2f2498a63ab508bf68c5f3c9", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_behaviour ) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9e82e62c6..e9d39a722 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -28,7 +28,7 @@ [dev-dependencies.async-std] features = ["alloc", "async-attributes", "async-channel", "async-global-executor", "async-io", "async-lock", "attributes", "crossbeam-utils", "default", "futures-channel", "futures-core", "futures-io", "futures-lite", "gloo-timers", "kv-log-macro", "log", "memchr", "once_cell", "pin-project-lite", "pin-utils", "slab", "std", "wasm-bindgen-futures"] - version = "1.13.0" + version = "1.13.1" default-features = false [dev-dependencies.steps] @@ -55,18 +55,18 @@ [dependencies.tokio] features = ["bytes", "default", "fs", "full", "io-std", "io-util", "libc", "macros", "mio", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "time", "tokio-macros"] - version = "1.43.0" + version = "1.44.1" default-features = false [dependencies.typedb-protocol] features = [] + rev = "7df6a6bfbbbd29940558ae7940b2065e1e2ba0b1" git = "https://github.com/typedb/typedb-protocol" - tag = "3.0.0" default-features = false [dependencies.log] features = ["kv", "kv_unstable", "std", "value-bag"] - version = "0.4.26" + version = "0.4.27" default-features = false [dependencies.tokio-stream] @@ -81,7 +81,7 @@ [dependencies.uuid] features = ["default", "fast-rng", "rng", "serde", "std", "v4"] - version = "1.15.1" + version = "1.16.0" default-features = false [dependencies.itertools] @@ -106,7 +106,7 @@ [dependencies.http] features = ["default", "std"] - version = "1.2.0" + version = "1.3.1" default-features = false [dependencies.maybe-async] diff --git a/rust/src/common/error.rs b/rust/src/common/error.rs index 6c8a284ca..c64a659d7 100644 --- a/rust/src/common/error.rs +++ b/rust/src/common/error.rs @@ -21,7 +21,7 @@ use std::{collections::HashSet, error::Error as StdError, fmt}; use itertools::Itertools; use tonic::{Code, Status}; -use tonic_types::StatusExt; +use tonic_types::{ErrorDetails, ErrorInfo, StatusExt}; use super::{address::Address, RequestID}; @@ -150,7 +150,7 @@ error_messages! { ConnectionError 15: "The replica is not the primary replica.", ClusterAllNodesFailed { errors: String } = 16: "Attempted connecting to all TypeDB Cluster servers, but the following errors occurred: \n{errors}.", - ClusterTokenCredentialInvalid = + TokenCredentialInvalid = 17: "Invalid token credentials.", EncryptionSettingsMismatch = 18: "Unable to connect to TypeDB: possible encryption settings mismatch.", @@ -275,6 +275,16 @@ impl Error { } } + fn try_extracting_connection_error(status: &Status, code: &str) -> Option { + // TODO: We should probably catch more connection errors instead of wrapping them into + // ServerErrors. However, the most valuable information even for connection is inside + // stacktraces now. + match code { + "AUT3" => Some(ConnectionError::TokenCredentialInvalid {}), + _ => None, + } + } + fn from_message(message: &str) -> Self { // TODO: Consider converting some of the messages to connection errors Self::Other(message.to_owned()) @@ -352,9 +362,13 @@ impl From for Error { }) } else if let Some(error_info) = details.error_info() { let code = error_info.reason.clone(); + if let Some(connection_error) = Self::try_extracting_connection_error(&status, &code) { + return Self::Connection(connection_error); + } let domain = error_info.domain.clone(); let stack_trace = if let Some(debug_info) = details.debug_info() { debug_info.stack_entries.clone() } else { vec![] }; + Self::Server(ServerError::new(code, domain, status.message().to_owned(), stack_trace)) } else { Self::from_message(status.message()) @@ -364,7 +378,6 @@ impl From for Error { Self::parse_unavailable(status.message()) } else if status.code() == Code::Unknown || is_rst_stream(&status) - || status.code() == Code::InvalidArgument || status.code() == Code::FailedPrecondition || status.code() == Code::AlreadyExists { diff --git a/rust/src/connection/message.rs b/rust/src/connection/message.rs index 73f860b0c..e9e080798 100644 --- a/rust/src/connection/message.rs +++ b/rust/src/connection/message.rs @@ -35,12 +35,12 @@ use crate::{ error::ServerError, info::UserInfo, user::User, - Options, TransactionType, + Credentials, Options, TransactionType, }; #[derive(Debug)] pub(super) enum Request { - ConnectionOpen { driver_lang: String, driver_version: String }, + ConnectionOpen { driver_lang: String, driver_version: String, credentials: Credentials }, ServersAll, diff --git a/rust/src/connection/network/channel.rs b/rust/src/connection/network/channel.rs index 1d972bfc9..a96ec9bdd 100644 --- a/rust/src/connection/network/channel.rs +++ b/rust/src/connection/network/channel.rs @@ -17,11 +17,12 @@ * under the License. */ -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use tonic::{ body::BoxBody, client::GrpcService, + metadata::MetadataValue, service::{ interceptor::{InterceptedService, ResponseFuture as InterceptorResponseFuture}, Interceptor, @@ -65,20 +66,33 @@ pub(super) fn open_callcred_channel( #[derive(Debug)] pub(super) struct CallCredentials { credentials: Credentials, + token: RwLock>, } impl CallCredentials { pub(super) fn new(credentials: Credentials) -> Self { - Self { credentials } + Self { credentials, token: RwLock::new(None) } } - pub(super) fn username(&self) -> &str { - self.credentials.username() + pub(super) fn credentials(&self) -> &Credentials { + &self.credentials + } + + pub(super) fn set_token(&self, token: String) { + *self.token.write().expect("Expected token write lock acquisition on set") = Some(token); + } + + pub(super) fn reset_token(&self) { + *self.token.write().expect("Expected token write lock acquisition on reset") = None; } pub(super) fn inject(&self, mut request: Request<()>) -> Request<()> { - request.metadata_mut().insert("username", self.credentials.username().try_into().unwrap()); - request.metadata_mut().insert("password", self.credentials.password().try_into().unwrap()); + if let Some(token) = &*self.token.read().expect("Expected token read lock acquisition on inject") { + request.metadata_mut().insert( + "authorization", + format!("Bearer {}", token).try_into().expect("Expected authorization header formatting"), + ); + } request } } diff --git a/rust/src/connection/network/proto/message.rs b/rust/src/connection/network/proto/message.rs index d03ff4023..f62768bdb 100644 --- a/rust/src/connection/network/proto/message.rs +++ b/rust/src/connection/network/proto/message.rs @@ -19,8 +19,8 @@ use itertools::Itertools; use typedb_protocol::{ - connection, database, database_manager, query::initial_res::Res, server_manager, transaction, user, user_manager, - Version::Version, + authentication, connection, database, database_manager, query::initial_res::Res, server_manager, transaction, user, + user_manager, Version::Version, }; use uuid::Uuid; @@ -32,14 +32,18 @@ use crate::{ error::{ConnectionError, InternalError, ServerError}, info::UserInfo, user::User, + Credentials, }; impl TryIntoProto for Request { fn try_into_proto(self) -> Result { match self { - Self::ConnectionOpen { driver_lang, driver_version } => { - Ok(connection::open::Req { version: Version.into(), driver_lang, driver_version }) - } + Self::ConnectionOpen { driver_lang, driver_version, credentials } => Ok(connection::open::Req { + version: Version.into(), + driver_lang, + driver_version, + authentication: Some(credentials.try_into_proto()?), + }), other => Err(InternalError::UnexpectedRequestType { request_type: format!("{other:?}") }.into()), } } @@ -225,14 +229,28 @@ impl TryIntoProto for Request { } } +impl TryIntoProto for Credentials { + fn try_into_proto(self) -> Result { + Ok(authentication::token::create::Req { + credentials: Some(authentication::token::create::req::Credentials::Password( + authentication::token::create::req::Password { + username: self.username().to_owned(), + password: self.password().to_owned(), + }, + )), + }) + } +} + impl TryFromProto for Response { fn try_from_proto(proto: connection::open::Res) -> Result { let mut database_infos = Vec::new(); - for database_info_proto in proto.databases_all.unwrap().databases { + for database_info_proto in proto.databases_all.expect("Expected databases data").databases { database_infos.push(DatabaseInfo::try_from_proto(database_info_proto)?); } Ok(Self::ConnectionOpen { - connection_id: Uuid::from_slice(proto.connection_id.unwrap().id.as_slice()).unwrap(), + connection_id: Uuid::from_slice(proto.connection_id.expect("Expected connection id").id.as_slice()) + .unwrap(), server_duration_millis: proto.server_duration_millis, databases: database_infos, }) diff --git a/rust/src/connection/network/stub.rs b/rust/src/connection/network/stub.rs index 38af93adf..db4492c95 100644 --- a/rust/src/connection/network/stub.rs +++ b/rust/src/connection/network/stub.rs @@ -25,12 +25,15 @@ use tokio::sync::mpsc::{unbounded_channel as unbounded_async, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{Response, Status, Streaming}; use typedb_protocol::{ - connection, database, database_manager, server_manager, transaction, type_db_client::TypeDbClient as GRPC, user, - user_manager, + authentication, connection, database, database_manager, server_manager, transaction, + type_db_client::TypeDbClient as GRPC, user, user_manager, }; use super::channel::{CallCredentials, GRPCChannel}; -use crate::common::{error::ConnectionError, Error, Result, StdResult}; +use crate::{ + common::{error::ConnectionError, Error, Result, StdResult}, + connection::network::proto::TryIntoProto, +}; type TonicResult = StdResult, Status>; @@ -45,15 +48,41 @@ impl RPCStub { Self { grpc: GRPC::new(channel), call_credentials } } - async fn call(&mut self, call: F) -> Result + async fn call_with_auto_renew_token(&mut self, call: F) -> Result where for<'a> F: Fn(&'a mut Self) -> BoxFuture<'a, Result>, { - call(self).await + match call(self).await { + Err(Error::Connection(ConnectionError::TokenCredentialInvalid)) => { + debug!("Request rejected because token credential was invalid. Renewing token and trying again..."); + self.renew_token().await?; + call(self).await + } + res => res, + } + } + + async fn renew_token(&mut self) -> Result { + if let Some(call_credentials) = &self.call_credentials { + trace!("Renewing token..."); + call_credentials.reset_token(); + let request = call_credentials.credentials().clone().try_into_proto()?; + let token = self.grpc.authentication_token_create(request).await?.into_inner().token; + call_credentials.set_token(token); + trace!("Token renewed"); + } + Ok(()) } pub(super) async fn connection_open(&mut self, req: connection::open::Req) -> Result { - self.single(|this| Box::pin(this.grpc.connection_open(req.clone()))).await + let result = self.single(|this| Box::pin(this.grpc.connection_open(req.clone()))).await; + if let Ok(response) = &result { + if let Some(call_credentials) = &self.call_credentials { + call_credentials + .set_token(response.authentication.as_ref().expect("Expected authentication token").token.clone()); + } + } + result } pub(super) async fn servers_all(&mut self, req: server_manager::all::Req) -> Result { @@ -107,7 +136,7 @@ impl RPCStub { &mut self, open_req: transaction::Req, ) -> Result<(UnboundedSender, Streaming)> { - self.call(|this| { + self.call_with_auto_renew_token(|this| { let transaction_req = transaction::Client { reqs: vec![open_req.clone()] }; Box::pin(async { let (sender, receiver) = unbounded_async(); @@ -154,6 +183,6 @@ impl RPCStub { for<'a> F: Fn(&'a mut Self) -> BoxFuture<'a, TonicResult> + Send + Sync, R: 'static, { - self.call(|this| Box::pin(call(this).map(|r| Ok(r?.into_inner())))).await + self.call_with_auto_renew_token(|this| Box::pin(call(this).map(|r| Ok(r?.into_inner())))).await } } diff --git a/rust/src/connection/server_connection.rs b/rust/src/connection/server_connection.rs index 58c0ed211..d92c25c3d 100644 --- a/rust/src/connection/server_connection.rs +++ b/rust/src/connection/server_connection.rs @@ -65,9 +65,9 @@ impl ServerConnection { ) -> crate::Result<(Self, Vec)> { let username = credentials.username().to_string(); let request_transmitter = - Arc::new(RPCTransmitter::start(address, credentials, driver_options, &background_runtime)?); + Arc::new(RPCTransmitter::start(address, credentials.clone(), driver_options, &background_runtime)?); let (connection_id, latency, database_info) = - Self::open_connection(&request_transmitter, driver_lang, driver_version).await?; + Self::open_connection(&request_transmitter, driver_lang, driver_version, credentials).await?; let latency_tracker = LatencyTracker::new(latency); let server_connection = Self { background_runtime, @@ -85,9 +85,13 @@ impl ServerConnection { request_transmitter: &RPCTransmitter, driver_lang: &str, driver_version: &str, + credentials: Credentials, ) -> crate::Result<(Uuid, Duration, Vec)> { - let message = - Request::ConnectionOpen { driver_lang: driver_lang.to_owned(), driver_version: driver_version.to_owned() }; + let message = Request::ConnectionOpen { + driver_lang: driver_lang.to_owned(), + driver_version: driver_version.to_owned(), + credentials, + }; let request_time = Instant::now(); match request_transmitter.request(message).await? { diff --git a/rust/tests/behaviour/steps/Cargo.toml b/rust/tests/behaviour/steps/Cargo.toml index 9429cf8d5..2d2a97434 100644 --- a/rust/tests/behaviour/steps/Cargo.toml +++ b/rust/tests/behaviour/steps/Cargo.toml @@ -16,7 +16,7 @@ features = {} [dependencies.tokio] features = ["bytes", "default", "fs", "full", "io-std", "io-util", "libc", "macros", "mio", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "time", "tokio-macros"] - version = "1.43.0" + version = "1.44.1" default-features = false [dependencies.smol] @@ -36,7 +36,7 @@ features = {} [dependencies.async-std] features = ["alloc", "async-attributes", "async-channel", "async-global-executor", "async-io", "async-lock", "attributes", "crossbeam-utils", "default", "futures-channel", "futures-core", "futures-io", "futures-lite", "gloo-timers", "kv-log-macro", "log", "memchr", "once_cell", "pin-project-lite", "pin-utils", "slab", "std", "wasm-bindgen-futures"] - version = "1.13.0" + version = "1.13.1" default-features = false [dependencies.macro_rules_attribute] @@ -61,7 +61,7 @@ features = {} [dependencies.uuid] features = ["default", "fast-rng", "rng", "serde", "std", "v4"] - version = "1.15.1" + version = "1.16.0" default-features = false [dependencies.itertools] diff --git a/rust/tests/behaviour/steps/params.rs b/rust/tests/behaviour/steps/params.rs index e92d52107..d8529c5dc 100644 --- a/rust/tests/behaviour/steps/params.rs +++ b/rust/tests/behaviour/steps/params.rs @@ -27,52 +27,6 @@ use typedb_driver::{ TransactionType as TypeDBTransactionType, }; -#[derive(Debug, Parameter)] -#[param(name = "containment", regex = r"(?:do not )?contain")] -pub struct ContainmentParam(bool); - -impl ContainmentParam { - pub fn assert(&self, actuals: &[T], item: U) - where - T: Comparable + fmt::Debug, - U: PartialEq + fmt::Debug, - { - if self.0 { - assert!(actuals.iter().any(|actual| actual.equals(&item)), "{item:?} not found in {actuals:?}") - } else { - assert!(actuals.iter().all(|actual| !actual.equals(&item)), "{item:?} found in {actuals:?}") - } - } -} - -impl FromStr for ContainmentParam { - type Err = Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self(s == "contain")) - } -} - -pub trait Comparable { - fn equals(&self, item: &U) -> bool; -} - -impl, U: PartialEq + ?Sized> Comparable<&U> for T { - fn equals(&self, item: &&U) -> bool { - self.borrow() == *item - } -} - -impl<'a, T1, T2, U1, U2> Comparable<(&'a U1, &'a U2)> for (T1, T2) -where - T1: Comparable<&'a U1>, - T2: Comparable<&'a U2>, -{ - fn equals(&self, (first, second): &(&'a U1, &'a U2)) -> bool { - self.0.equals(first) && self.1.equals(second) - } -} - #[derive(Debug, Default, Parameter, Clone)] #[param(name = "value", regex = ".*?")] pub(crate) struct Value { @@ -325,7 +279,10 @@ impl MayError { pub fn check(&self, res: Result) -> Option { match self { MayError::False => Some(res.unwrap()), - MayError::True(None) => None, + MayError::True(None) => { + let _ = res.unwrap_err(); + None + }, MayError::True(Some(expected_message)) => { let actual_error = res.unwrap_err(); let actual_message = actual_error.to_string(); diff --git a/tool/test/start-community-server.sh b/tool/test/start-community-server.sh index 7695fb53d..fe52d1596 100755 --- a/tool/test/start-community-server.sh +++ b/tool/test/start-community-server.sh @@ -22,6 +22,7 @@ rm -rf typedb-all bazel run //tool/test:typedb-extractor -- typedb-all BAZEL_JAVA_HOME=$(bazel run //tool/test:echo-java-home) +# TODO: Can add `--server.authentication.token_ttl_seconds 15` to test token auto-renewal in BDDs! ./typedb-all/typedb server --development-mode.enabled & set +e