pub struct ApplicationNodeBuilder<K: KeyCustody = NoOpCustody, D: DidMethod = NoOpDidMethod, S: Storage = NoOpStorage, Dom = NoDomain, Id = NoIdentity> { /* private fields */ }Expand description
Builder for ApplicationNode.
Uses a type-state pattern to enforce required fields at compile time.
The builder starts with Dom = NoDomain, Id = NoIdentity. Calling
domain transitions Dom to HasDomain, and calling
generate_identity_with,
identity, or
identity_with_storage transitions Id
to HasIdentity. build is only available when both
are set.
§Required fields
domain– the domain this node serves.- Identity – one of
generate_identity_with,identity, oridentity_with_storage.
§Optional fields
Implementations§
Source§impl ApplicationNodeBuilder
impl ApplicationNodeBuilder
Sourcepub fn new() -> Self
pub fn new() -> Self
Creates a new builder with all fields unset.
The relay uses BlobStorageBackend::default() (in-memory) by default. Call
blob_storage to use a different backend.
Source§impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: Storage + 'static, Id> ApplicationNodeBuilder<K, D, S, NoDomain, Id>
impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: Storage + 'static, Id> ApplicationNodeBuilder<K, D, S, NoDomain, Id>
Sourcepub fn domain(
self,
domain: &str,
) -> ApplicationNodeBuilder<K, D, S, HasDomain, Id>
pub fn domain( self, domain: &str, ) -> ApplicationNodeBuilder<K, D, S, HasDomain, Id>
Sets the domain this node serves.
The relay URL is derived as wss://<domain>/scp/v1 (spec section
18.5.2). Either .domain() or .no_domain() must be called —
the builder cannot be built without one (§10.12.8).
Sourcepub fn no_domain(self) -> ApplicationNodeBuilder<K, D, S, HasNoDomain, Id>
pub fn no_domain(self) -> ApplicationNodeBuilder<K, D, S, HasNoDomain, Id>
Zero-config NAT-traversed mode (§10.12.8).
When set: skip ACME TLS provisioning, probe NAT type via STUN,
attempt UPnP (Tier 1), fallback to STUN address (Tier 2),
register with bridge (Tier 3), publish DID document with ws://
relay URL, do NOT serve .well-known/scp.
This is the zero-config deployment path for self-hosted relays behind residential NAT.
Source§impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: Storage + 'static, Dom, Id> ApplicationNodeBuilder<K, D, S, Dom, Id>
impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: Storage + 'static, Dom, Id> ApplicationNodeBuilder<K, D, S, Dom, Id>
Sourcepub const fn bind_addr(self, addr: SocketAddr) -> Self
pub const fn bind_addr(self, addr: SocketAddr) -> Self
Sets the socket address for the relay server to bind to.
Defaults to 127.0.0.1:0 (OS-assigned port) if not specified.
Sourcepub fn acme_email(self, email: &str) -> Self
pub fn acme_email(self, email: &str) -> Self
Sets the ACME email for TLS certificate provisioning.
Used for Let’s Encrypt certificate requests (spec section 18.6.3).
When .domain() is set, the email is passed to
AcmeProvider during build() for ACME account
registration (SCP-246). Optional – if omitted, the ACME account is
created without a contact email.
Sourcepub fn stun_server(self, url: &str) -> Self
pub fn stun_server(self, url: &str) -> Self
Override the STUN endpoint used for NAT type probing (§10.12.8).
Default: bootstrap relay with STUN support. The value should be a
socket address (e.g., "stun.l.google.com:19302").
Sourcepub fn bridge_relay(self, url: &str) -> Self
pub fn bridge_relay(self, url: &str) -> Self
Override the bridge relay used for Tier 3 fallback (§10.12.8).
Default: first bridge-capable relay in the fallback relay list.
The value should be a wss:// URL.
Sourcepub fn nat_strategy(self, strategy: Arc<dyn NatStrategy>) -> Self
pub fn nat_strategy(self, strategy: Arc<dyn NatStrategy>) -> Self
Sets a custom NAT strategy for testability.
Production code uses DefaultNatStrategy (created automatically
during build()). Tests can inject mock strategies.
Sourcepub fn port_mapper(self, mapper: Arc<dyn PortMapper>) -> Self
pub fn port_mapper(self, mapper: Arc<dyn PortMapper>) -> Self
Sets a UPnP/NAT-PMP port mapper for Tier 1 NAT traversal (spec 10.12.2).
When set, the DefaultNatStrategy will attempt UPnP port mapping
before falling through to STUN (Tier 2). Has no effect if a custom
NatStrategy is provided via nat_strategy.
Sourcepub fn reachability_probe(self, probe: Arc<dyn ReachabilityProbe>) -> Self
pub fn reachability_probe(self, probe: Arc<dyn ReachabilityProbe>) -> Self
Sets a reachability probe for self-test verification (SCP-242).
The self-test verifies that an external address is actually reachable
before publishing it in the DID document (spec 10.12.2 step 4). When
not set, the DefaultNatStrategy constructs a
DefaultReachabilityProbe
from the first configured STUN endpoint. Has no effect if a custom
NatStrategy is provided via nat_strategy.
Sourcepub fn tls_provider(self, provider: Arc<dyn TlsProvider>) -> Self
pub fn tls_provider(self, provider: Arc<dyn TlsProvider>) -> Self
Sets a custom TLS provider for testability.
Production code uses AcmeProvider (created
automatically during domain build()). Tests can inject mock
providers that succeed or fail deterministically.
Sourcepub fn network_detector(self, detector: Arc<dyn NetworkChangeDetector>) -> Self
pub fn network_detector(self, detector: Arc<dyn NetworkChangeDetector>) -> Self
Sets a network change detector for tier re-evaluation (§10.12.1, SCP-243).
When provided, network change events (IP change, interface up/down) trigger immediate re-evaluation of the reachability tier. Without a detector, only the periodic 30-minute timer triggers re-evaluation.
Use ChannelNetworkChangeDetector
for channel-based event injection, or implement
NetworkChangeDetector
for platform-specific detection.
Sourcepub fn local_api(self, addr: SocketAddr) -> Self
pub fn local_api(self, addr: SocketAddr) -> Self
Enables the local dev API on the specified address.
When set, a bearer token is generated at build time and logged at
INFO level. The dev API listens on a separate port from the public
HTTPS listener, typically bound to 127.0.0.1:<port>.
If not called, the dev API is disabled (production default).
See spec section 18.10.2 and 18.10.5.
§Panics
Panics if addr is not a loopback address (127.0.0.1 or ::1).
The dev API must never be exposed on a non-loopback interface.
Sourcepub const fn http_bind_addr(self, addr: SocketAddr) -> Self
pub const fn http_bind_addr(self, addr: SocketAddr) -> Self
Sets the bind address for the public HTTP server.
This is the address where ApplicationNode::serve listens for
incoming HTTP/HTTPS connections (.well-known/scp, /scp/v1
WebSocket upgrade, broadcast projection endpoints, and any
application routes).
This is distinct from the relay’s internal bind address (set via
bind_addr), which is a localhost-only listener
used for the internal WebSocket bridge.
Defaults to DEFAULT_HTTP_BIND_ADDR (0.0.0.0:8443) if not specified.
Sourcepub fn cors_origins(self, origins: Vec<String>) -> Self
pub fn cors_origins(self, origins: Vec<String>) -> Self
Sets the allowed CORS origins for public endpoints.
Public endpoints (.well-known/scp, broadcast projection feeds and
messages) include Access-Control-Allow-Origin headers so that
browser-based JavaScript and WASM clients can read responses
cross-origin.
- If not called, or called with an empty list: permissive CORS
(
Access-Control-Allow-Origin: *). This is the default because broadcast content is public by design (spec section 18.11.6). - If called with a non-empty list: restricts to exactly those
origins (e.g.,
["https://example.com"]).
CORS is NOT applied to the WebSocket relay endpoint (/scp/v1)
because WebSocket upgrades have their own origin mechanism, nor to
the dev API (localhost-only).
See issue #231.
Sourcepub const fn projection_rate_limit(self, rate: u32) -> Self
pub const fn projection_rate_limit(self, rate: u32) -> Self
Sets the per-IP rate limit for broadcast projection endpoints.
Controls the maximum number of requests per second from a single IP
address to the /scp/broadcast/* endpoints. Exceeding this rate
returns HTTP 429 Too Many Requests.
Default: 60 req/s. Also configurable via SCP_NODE_PROJECTION_RATE_LIMIT.
See spec section 18.11.6.
Source§impl<K: KeyCustody + 'static, D: DidMethod + 'static, Dom, Id> ApplicationNodeBuilder<K, D, NoOpStorage, Dom, Id>
impl<K: KeyCustody + 'static, D: DidMethod + 'static, Dom, Id> ApplicationNodeBuilder<K, D, NoOpStorage, Dom, Id>
Sourcepub fn storage<S2: Storage + 'static>(
self,
storage: S2,
) -> ApplicationNodeBuilder<K, D, S2, Dom, Id>
pub fn storage<S2: Storage + 'static>( self, storage: S2, ) -> ApplicationNodeBuilder<K, D, S2, Dom, Id>
Sets an explicit storage backend.
If not called, .build() uses a default no-op storage.
Source§impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: Storage + 'static, Dom, Id> ApplicationNodeBuilder<K, D, S, Dom, Id>
impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: Storage + 'static, Dom, Id> ApplicationNodeBuilder<K, D, S, Dom, Id>
Sourcepub fn blob_storage(self, blob_storage: impl Into<BlobStorageBackend>) -> Self
pub fn blob_storage(self, blob_storage: impl Into<BlobStorageBackend>) -> Self
Sets a custom blob storage backend for the relay server.
If not called, the relay uses in-memory storage (all blobs lost on restart).
Accepts any type that converts into BlobStorageBackend.
Source§impl<S: Storage + 'static, Dom> ApplicationNodeBuilder<NoOpCustody, NoOpDidMethod, S, Dom, NoIdentity>
impl<S: Storage + 'static, Dom> ApplicationNodeBuilder<NoOpCustody, NoOpDidMethod, S, Dom, NoIdentity>
Sourcepub fn identity<D2: DidMethod + 'static>(
self,
identity: ScpIdentity,
document: DidDocument,
did_method: Arc<D2>,
) -> ApplicationNodeBuilder<NoOpCustody, D2, S, Dom, HasIdentity>
pub fn identity<D2: DidMethod + 'static>( self, identity: ScpIdentity, document: DidDocument, did_method: Arc<D2>, ) -> ApplicationNodeBuilder<NoOpCustody, D2, S, Dom, HasIdentity>
Sets an explicit identity and DID document to use.
The identity will be published to the DHT with SCPRelay entries
pointing to this node’s relay URL.
Sourcepub fn generate_identity_with<K2: KeyCustody + 'static, D2: DidMethod + 'static>(
self,
key_custody: Arc<K2>,
did_method: Arc<D2>,
) -> ApplicationNodeBuilder<K2, D2, S, Dom, HasIdentity>
pub fn generate_identity_with<K2: KeyCustody + 'static, D2: DidMethod + 'static>( self, key_custody: Arc<K2>, did_method: Arc<D2>, ) -> ApplicationNodeBuilder<K2, D2, S, Dom, HasIdentity>
Configures the builder to generate a new DID identity on .build().
Uses the provided key custody and DID method implementations.
Sourcepub fn identity_with_storage<K2: KeyCustody + 'static, D2: DidMethod + 'static>(
self,
key_custody: Arc<K2>,
did_method: Arc<D2>,
) -> ApplicationNodeBuilder<K2, D2, S, Dom, HasIdentity>
pub fn identity_with_storage<K2: KeyCustody + 'static, D2: DidMethod + 'static>( self, key_custody: Arc<K2>, did_method: Arc<D2>, ) -> ApplicationNodeBuilder<K2, D2, S, Dom, HasIdentity>
Configures the builder to automatically persist and reload identity.
This is the recommended way to manage identity for long-lived
applications. On the first run it behaves like
generate_identity_with: it creates a
new DID via the provided key_custody and did_method, then persists
the resulting ScpIdentity and DidDocument to the builder’s
storage backend under the key "scp/identity".
On subsequent runs with the same storage backend, the persisted identity is deserialized and reused — no new DID is created. This ensures the node keeps the same DID across restarts.
§Lifecycle
- At
.build()time, check storage for key"scp/identity". - Found: Deserialize the stored
ScpIdentityandDidDocument, use the.identity()path internally. - Not found: Call
did_method.create(custody), persist the result to storage, then continue.
[KeyHandle] indices remain valid across restarts because the custody
backend (e.g., FileKeyCustody) reloads the same keyring from disk.
§Requirements
A real storage backend must be set via .storage()
before calling .build(). The default no-op storage will not persist
anything.
§Example
// let custody = Arc::new(FileKeyCustody::open("keys.db")?);
// let did_method = Arc::new(DidDht::new(...));
// let storage = SqliteStorage::open("node.db")?;
//
// let node = ApplicationNodeBuilder::new()
// .storage(storage)
// .domain("example.com")
// .identity_with_storage(custody, did_method)
// .build()
// .await?;
// // First run: creates new DID, persists to storage.
// // Subsequent runs: reloads same DID from storage.Source§impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: EncryptedStorage + 'static> ApplicationNodeBuilder<K, D, S, HasDomain, HasIdentity>
impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: EncryptedStorage + 'static> ApplicationNodeBuilder<K, D, S, HasDomain, HasIdentity>
Sourcepub async fn build(self) -> Result<ApplicationNode<S>, NodeError>
pub async fn build(self) -> Result<ApplicationNode<S>, NodeError>
Builds the ApplicationNode.
Requires S: EncryptedStorage — compile-time enforcement that
the storage backend encrypts data at rest. For testing with
unencrypted backends, use build_for_testing.
See issue #695.
§Errors
Returns NodeError if storage, identity, relay, or TLS setup fails.
Source§impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: EncryptedStorage + 'static> ApplicationNodeBuilder<K, D, S, HasNoDomain, HasIdentity>
impl<K: KeyCustody + 'static, D: DidMethod + 'static, S: EncryptedStorage + 'static> ApplicationNodeBuilder<K, D, S, HasNoDomain, HasIdentity>
Sourcepub async fn build(self) -> Result<ApplicationNode<S>, NodeError>
pub async fn build(self) -> Result<ApplicationNode<S>, NodeError>
Builds the ApplicationNode in zero-config no-domain mode (§10.12.8).
Requires S: EncryptedStorage. For testing, use
build_for_testing.
§Errors
Returns NodeError if storage, identity, relay, or NAT setup fails.
Auto Trait Implementations§
impl<K, D, S, Dom, Id> Freeze for ApplicationNodeBuilder<K, D, S, Dom, Id>where
S: Freeze,
impl<K = NoOpCustody, D = NoOpDidMethod, S = NoOpStorage, Dom = NoDomain, Id = NoIdentity> !RefUnwindSafe for ApplicationNodeBuilder<K, D, S, Dom, Id>
impl<K, D, S, Dom, Id> Send for ApplicationNodeBuilder<K, D, S, Dom, Id>
impl<K, D, S, Dom, Id> Sync for ApplicationNodeBuilder<K, D, S, Dom, Id>
impl<K, D, S, Dom, Id> Unpin for ApplicationNodeBuilder<K, D, S, Dom, Id>
impl<K, D, S, Dom, Id> UnsafeUnpin for ApplicationNodeBuilder<K, D, S, Dom, Id>where
S: UnsafeUnpin,
impl<K = NoOpCustody, D = NoOpDidMethod, S = NoOpStorage, Dom = NoDomain, Id = NoIdentity> !UnwindSafe for ApplicationNodeBuilder<K, D, S, Dom, Id>
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Declassify for T
impl<T> Declassify for T
type Declassified = T
fn declassify(self) -> T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more