Developer docs/Guides
Gateways, provisioning and registrars
These addon kinds use the same WASM contract as other addons. Each one implements a fixed set of ops, and the SDK provides a macro so you only implement a Rust trait. The traits below are copied from the source.
#Payment gateways
Use pelty_addon_sdk::gateway_addon!. The host-side trait:
crates/pelty-payments/src/lib.rs · pub trait PaymentGatewayRust
/// The stable gateway contract.
pub trait PaymentGateway: Send + Sync {
fn id(&self) -> &'static str;
fn display_name(&self) -> &'static str;
fn capabilities(&self) -> Capabilities;
fn create_payment<'a>(
&'a self,
req: &'a PaymentRequest,
) -> BoxFuture<'a, Result<PaymentInit, GatewayError>>;
/// Verify the signature and parse the event. `now` is unix seconds.
fn parse_webhook(
&self,
headers: &Headers,
body: &[u8],
now: i64,
) -> Result<WebhookEvent, GatewayError>;
/// Async webhook entry point used by the HTTP layer. Defaults to
/// [`parse_webhook`](Self::parse_webhook); gateways whose webhooks carry no
/// signature (Mollie) override it to fetch the authoritative state.
fn handle_webhook<'a>(
&'a self,
headers: &'a Headers,
body: &'a [u8],
now: i64,
) -> BoxFuture<'a, Result<WebhookEvent, GatewayError>> {
Box::pin(async move { self.parse_webhook(headers, body, now) })
}
/// Finish a payment after the payer returned from the provider (PayPal
/// capture, or a status poll). Default: not needed (webhook-driven).
fn complete_payment<'a>(
&'a self,
_provider_ref: &'a str,
_query: &'a HashMap<String, String>,
) -> BoxFuture<'a, Result<CompleteStatus, GatewayError>> {
Box::pin(async { Err(GatewayError::Unsupported) })
}
fn refund<'a>(
&'a self,
provider_ref: &'a str,
amount: Money,
idempotency_key: &'a str,
) -> BoxFuture<'a, Result<RefundResult, GatewayError>>;
/// Attach a client-side created method token (e.g. a Stripe `pm_…`) for
/// future off-session use. Gateways without `recurring` keep the default.
fn save_method<'a>(
&'a self,
_token: &'a str,
_customer_email: &'a str,
_idempotency_key: &'a str,
) -> BoxFuture<'a, Result<SavedMethod, GatewayError>> {
Box::pin(async { Err(GatewayError::Unsupported) })
}
/// Charge a saved method off-session.
fn charge_saved<'a>(
&'a self,
_token: &'a str,
_req: &'a PaymentRequest,
) -> BoxFuture<'a, Result<ChargeOutcome, GatewayError>> {
Box::pin(async { Err(GatewayError::Unsupported) })
}
}#Provisioning modules
Use pelty_addon_sdk::provisioning_addon!:
crates/pelty-provisioning/src/lib.rs · pub trait ProvisioningModuleRust
pub trait ProvisioningModule: Send + Sync {
fn info(&self) -> ModuleInfo;
/// Fields asked at checkout for this module's products.
fn checkout_fields(&self) -> Vec<CheckoutField> {
Vec::new()
}
fn test_connection<'a>(&'a self, server: &'a ServerConn) -> BoxFuture<'a, ProvisionResult<()>>;
fn create<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<Created>>;
fn suspend<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
fn unsuspend<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
fn terminate<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
/// Apply the (new) product config in `svc.product_config` to the remote.
fn change_package<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
/// Client single sign-on: the URL to send the client to, carrying the
/// signed one-time `token` (see [`sso`]). `None` means unsupported. The
/// default uses the server's `sso_url` template; modules may override it
/// (e.g. to exchange the token for a panel-native login link).
fn sso_url<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx, token: &'a str) -> BoxFuture<'a, ProvisionResult<Option<String>>> {
Box::pin(async move { Ok(sso::template_url(server, svc, token)) })
}
/// Names of extra actions this module supports (custom buttons).
fn custom_actions(&self) -> &'static [&'static str] {
&[]
}
/// Run one extra action. `params` carries user input (e.g. a new
/// password or an image id). Returns module-specific output (a console
/// URL, a new password…) to show once.
fn custom<'a>(&'a self, _server: &'a ServerConn, _svc: &'a ServiceCtx, action: &'a str, _params: &'a Value) -> BoxFuture<'a, ProvisionResult<Value>> {
Box::pin(async move { Err(ProvisionError::Config(format!("action {action} is not supported by this module"))) })
}
/// Live resource usage / status for the client area (`None` = unsupported).
fn usage<'a>(&'a self, _server: &'a ServerConn, _svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<Option<Value>>> {
Box::pin(async move { Ok(None) })
}
/// Accounts that exist on the server, for import/sync (`None` = unsupported).
fn list_accounts<'a>(&'a self, _server: &'a ServerConn) -> BoxFuture<'a, ProvisionResult<Option<Vec<RemoteAccount>>>> {
Box::pin(async move { Ok(None) })
}
/// Vendor resale (MarketConnect-style): products this vendor sells,
/// with the reseller's cost. `None` = not a vendor module.
fn catalog<'a>(&'a self, _server: &'a ServerConn) -> BoxFuture<'a, ProvisionResult<Option<Vec<VendorProduct>>>> {
Box::pin(async move { Ok(None) })
}
}#Domain registrars
Use pelty_addon_sdk::registrar_addon!:
crates/pelty-provisioning/src/registrar.rs · pub trait RegistrarModuleRust
pub trait RegistrarModule: Send + Sync {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn available<'a>(&'a self, domain: &'a str) -> BoxFuture<'a, RegistrarResult<bool>>;
/// Register for `years`; `expires_on` is what the caller computed.
fn register<'a>(&'a self, domain: &'a str, years: u32, expires_on: &'a str, registrant: &'a Contact, nameservers: &'a [String]) -> BoxFuture<'a, RegistrarResult<()>>;
/// Register with the TLD's custom domain fields (`{key: value}` as set
/// up per TLD in Admin → Domain pricing). Default: fields are ignored.
fn register_with_fields<'a>(&'a self, domain: &'a str, years: u32, expires_on: &'a str, registrant: &'a Contact, nameservers: &'a [String], fields: &'a serde_json::Value) -> BoxFuture<'a, RegistrarResult<()>> {
let _ = fields;
self.register(domain, years, expires_on, registrant, nameservers)
}
fn renew<'a>(&'a self, domain: &'a str, years: u32, expires_on: &'a str) -> BoxFuture<'a, RegistrarResult<()>>;
fn transfer<'a>(&'a self, domain: &'a str, auth_code: &'a str, expires_on: &'a str) -> BoxFuture<'a, RegistrarResult<()>>;
/// Transfer with custom domain fields. Default: fields are ignored.
fn transfer_with_fields<'a>(&'a self, domain: &'a str, auth_code: &'a str, expires_on: &'a str, fields: &'a serde_json::Value) -> BoxFuture<'a, RegistrarResult<()>> {
let _ = fields;
self.transfer(domain, auth_code, expires_on)
}
fn set_nameservers<'a>(&'a self, domain: &'a str, nameservers: &'a [String]) -> BoxFuture<'a, RegistrarResult<()>>;
fn set_contact<'a>(&'a self, domain: &'a str, registrant: &'a Contact) -> BoxFuture<'a, RegistrarResult<()>>;
fn info<'a>(&'a self, domain: &'a str) -> BoxFuture<'a, RegistrarResult<RemoteDomain>>;
/// Transfer-out authorization (EPP) code. `None` = not supported.
fn epp_code<'a>(&'a self, _domain: &'a str) -> BoxFuture<'a, RegistrarResult<Option<String>>> {
Box::pin(async { Ok(None) })
}
/// Cost price list for TLD price sync. `None` = not supported.
fn tld_prices<'a>(&'a self) -> BoxFuture<'a, RegistrarResult<Option<Vec<TldCost>>>> {
Box::pin(async { Ok(None) })
}
/// Premium price of `domain`; `None` = standard pricing (or unsupported).
fn premium_price<'a>(&'a self, _domain: &'a str) -> BoxFuture<'a, RegistrarResult<Option<PremiumQuote>>> {
Box::pin(async { Ok(None) })
}
/// A copy configured with the registrar addon's settings (API keys…).
/// `None` = the module needs no settings and `self` is used as is.
fn configure(&self, _settings: &serde_json::Value) -> Option<Arc<dyn RegistrarModule>> {
None
}
}#SDK macros
extensions/addons/sdk/src/lib.rs · macro_rules! provisioning_addonRust
/// Export a provisioning addon from an expression building the module.
#[cfg(feature = "provisioning")]
#[macro_export]
macro_rules! provisioning_addon {
($module:expr) => {
struct PeltyAddon;
impl $crate::Guest for PeltyAddon {
fn describe() -> String {
$crate::provisioning::describe(&$module)
}
fn call(op: String, input: String) -> Result<String, String> {
$crate::provisioning::call(&$module, &op, &input)
}
fn on_event(_name: String, _payload: String) -> Result<(), String> {
Ok(())
}
}
$crate::export!(PeltyAddon with_types_in $crate);
};
}