Skip to main content

ic_custom_domains_canister_api/
lib.rs

1//! # Custom Domains Canister API
2//!
3//! This module defines the public API types and interfaces for the custom domains management canister.
4//! All types implement [`CandidType`] for integration with candid interface.
5
6use std::time::Duration;
7
8use candid::{CandidType, Principal};
9use derive_new::new;
10use serde::{Deserialize, Serialize};
11use strum::{EnumIter, IntoStaticStr};
12use thiserror::Error;
13
14type TaskId = u64;
15type UtcTimestamp = u64;
16
17// Declare constants related to the canister here, enabling usage in other modules and tests.
18
19// Certificate renewal should be attempted when this fraction of the validity period has elapsed
20pub const CERTIFICATE_VALIDITY_FRACTION: f64 = 0.66;
21
22// A domain is considered close to certificate expiration if less than this fraction of its validity period remains
23pub const CERT_EXPIRATION_ALERT_THRESHOLD: f64 = 0.2;
24
25// Task is considered timed out, if its result isn't submitted within this time window.
26// This allows the task to be rescheduled if a worker fails.
27// Submitting results for timed out tasks results in a NonExistingTaskSubmitted error.
28pub const TASK_TIMEOUT: Duration = Duration::from_secs(10 * 60);
29
30// If no certificate has been issued, the domain entry is removed after this duration.
31pub const UNREGISTERED_DOMAIN_EXPIRATION_TIME: Duration = Duration::from_secs(24 * 60 * 60);
32
33// If a domain has failed to renew and its certificate expired, it is removed after this duration.
34pub const EXPIRED_DOMAIN_EXPIRATION_TIME: Duration = Duration::from_secs(7 * 24 * 60 * 60);
35
36// If a task fails this many times with a recoverable error, it is no longer rescheduled.
37// User is expected to resubmit the task.
38pub const MAX_TASK_FAILURES: u32 = 20;
39
40// If a task fails, it will not be rescheduled earlier than this interval.
41pub const MIN_TASK_RETRY_DELAY: Duration = Duration::from_secs(30);
42
43// Default number of domains returned per page when no limit is specified or limit is zero
44pub const DEFAULT_PAGE_LIMIT: u32 = 100;
45
46// Maximum number of domains that can be returned in a single page to safely stay lower than 2MB response
47pub const MAX_PAGE_LIMIT: u32 = 400;
48
49// Interval for purging stale, unregistered domains
50pub const STALE_DOMAINS_CLEANUP_INTERVAL: Duration = Duration::from_secs(3 * 60 * 60);
51
52pub type FetchTaskResult = Result<Option<ScheduledTask>, FetchTaskError>;
53pub type SubmitTaskResult = Result<(), SubmitTaskError>;
54pub type TryAddTaskResult = Result<(), TryAddTaskError>;
55pub type GetDomainStatusResult = Result<Option<DomainStatus>, GetDomainStatusError>;
56pub type GetDomainEntryResult = Result<Option<DomainEntry>, GetDomainEntryError>;
57pub type GetLastChangeTimeResult = Result<UtcTimestamp, GetLastChangeTimeError>;
58pub type ListCertificatesPageResult = Result<CertificatesPage, ListCertificatesPageError>;
59pub type ListDomainsPageResult = Result<DomainsPage, ListDomainsPageError>;
60pub type HasNextTaskResult = Result<bool, HasNextTaskError>;
61
62#[derive(CandidType, Deserialize, Serialize, Clone, Debug)]
63pub struct InitArg {
64    pub authorized_principal: Option<Principal>,
65}
66
67#[derive(
68    CandidType, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, IntoStaticStr,
69)]
70#[strum(serialize_all = "snake_case")]
71pub enum TaskKind {
72    Issue,
73    Renew,
74    Update,
75    Delete,
76}
77
78#[derive(CandidType, Deserialize, Serialize, Clone, Debug)]
79pub struct InputTask {
80    pub kind: TaskKind,
81    pub domain: String,
82}
83
84#[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, new)]
85pub struct ScheduledTask {
86    pub kind: TaskKind,
87    pub domain: String,
88    pub id: TaskId,
89    pub enc_cert: Option<Vec<u8>>,
90}
91
92#[derive(CandidType, Deserialize, Serialize, Clone, Debug)]
93pub struct TaskResult {
94    pub domain: String,
95    pub outcome: TaskOutcome,
96    pub task_id: TaskId,
97    pub task_kind: TaskKind,
98    pub duration_secs: u64,
99}
100
101#[derive(CandidType, Deserialize, Serialize, Clone, Debug)]
102pub enum TaskOutcome {
103    Success(TaskOutput),
104    Failure(TaskFailReason),
105}
106
107#[derive(CandidType, Deserialize, Serialize, Clone, Debug)]
108pub enum TaskOutput {
109    Issue(IssueCertificateOutput),
110    Update(Principal),
111    Delete,
112}
113
114#[derive(CandidType, Deserialize, Serialize, Clone, Debug)]
115pub struct IssueCertificateOutput {
116    pub canister_id: Principal,
117    pub enc_cert: Vec<u8>,
118    pub enc_priv_key: Vec<u8>,
119    pub not_before: UtcTimestamp,
120    pub not_after: UtcTimestamp,
121}
122
123#[derive(CandidType, Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Error, IntoStaticStr)]
124#[strum(serialize_all = "snake_case")]
125pub enum TaskFailReason {
126    #[error("validation_failed: {0}")]
127    ValidationFailed(String),
128    #[error("timeout after {duration_secs}s")]
129    Timeout { duration_secs: UtcTimestamp },
130    #[error("rate_limited")]
131    RateLimited,
132    #[error("generic_failure: {0}")]
133    GenericFailure(String),
134}
135
136#[derive(CandidType, Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
137pub struct DomainStatus {
138    pub domain: String,
139    pub canister_id: Option<Principal>,
140    pub status: RegistrationStatus,
141}
142
143#[derive(CandidType, Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
144pub struct DomainEntry {
145    pub task: Option<TaskKind>,
146    // Timestamp when the task failed last time, if any
147    pub last_fail_time: Option<UtcTimestamp>,
148    // Reason for the last failure, if any
149    pub last_failure_reason: Option<TaskFailReason>,
150    // Number of consecutive failures for the current task (excluding rate limit failures)
151    pub failures_count: u32,
152    // Number of rate limit failures for the current task
153    pub rate_limit_failures_count: u32,
154    // Canister ID associated with the domain
155    pub canister_id: Option<Principal>,
156    // Timestamp when the domain entry was created (set once and never updated)
157    pub created_at: UtcTimestamp,
158    // Timestamp when the current task was taken by a worker
159    pub taken_at: Option<UtcTimestamp>,
160    // Timestamp when the current task was created
161    pub task_created_at: Option<UtcTimestamp>,
162    // PEM-encoded certificate data (encrypted)
163    pub enc_cert: Option<Vec<u8>>,
164    // PEM-encoded private key data (encrypted)
165    pub enc_priv_key: Option<Vec<u8>>,
166    // Certificate validity period start (as UNIX timestamp)
167    pub not_before: Option<UtcTimestamp>,
168    // Certificate validity period end (as UNIX timestamp)
169    pub not_after: Option<UtcTimestamp>,
170}
171
172/// Domain entry as returned by list_domains_page: includes domain name and all entry fields except enc_cert and enc_priv_key.
173#[derive(CandidType, Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
174pub struct ListedDomainEntry {
175    /// Fully qualified domain name (FQDN)
176    pub domain: String,
177    pub task: Option<TaskKind>,
178    pub last_fail_time: Option<UtcTimestamp>,
179    pub last_failure_reason: Option<TaskFailReason>,
180    pub failures_count: u32,
181    pub rate_limit_failures_count: u32,
182    pub canister_id: Option<Principal>,
183    pub created_at: UtcTimestamp,
184    pub taken_at: Option<UtcTimestamp>,
185    pub task_created_at: Option<UtcTimestamp>,
186    pub not_before: Option<UtcTimestamp>,
187    pub not_after: Option<UtcTimestamp>,
188}
189
190#[derive(
191    CandidType, Clone, Deserialize, Serialize, Debug, EnumIter, IntoStaticStr, PartialEq, Eq,
192)]
193#[strum(serialize_all = "snake_case")]
194pub enum RegistrationStatus {
195    /// The registration is currently being processed
196    Registering,
197    /// The domain has been successfully registered and has a valid certificate
198    Registered,
199    /// The domain registration has expired
200    Expired,
201    /// The registration failed with an error message
202    Failed(String),
203}
204
205#[derive(CandidType, Clone, Deserialize, Serialize, Debug)]
206pub struct CertificatesPage {
207    pub items: Vec<RegisteredDomain>,
208    pub next_key: Option<String>,
209}
210
211impl CertificatesPage {
212    pub fn new(items: Vec<RegisteredDomain>, next_key: Option<String>) -> Self {
213        Self { items, next_key }
214    }
215}
216
217#[derive(CandidType, Clone, Deserialize, Serialize, Debug)]
218pub struct ListCertificatesPageInput {
219    /// Optional starting point for pagination (domain name to start from)
220    pub start_key: Option<String>,
221    /// Maximum number of items to return per page
222    pub limit: Option<u32>,
223}
224
225impl ListCertificatesPageInput {
226    pub fn new() -> Self {
227        Self {
228            start_key: None,
229            limit: None,
230        }
231    }
232}
233
234impl Default for ListCertificatesPageInput {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240#[derive(CandidType, Clone, Deserialize, Serialize, Debug)]
241pub struct RegisteredDomain {
242    pub domain: String,
243    pub canister_id: Principal,
244    pub enc_cert: Vec<u8>,
245    pub enc_priv_key: Vec<u8>,
246}
247
248/// A page of domain entries with pagination information (items exclude enc_cert and enc_priv_key).
249#[derive(CandidType, Clone, Deserialize, Serialize, Debug)]
250pub struct DomainsPage {
251    pub items: Vec<ListedDomainEntry>,
252    pub next_key: Option<String>,
253}
254
255impl DomainsPage {
256    pub fn new(items: Vec<ListedDomainEntry>, next_key: Option<String>) -> Self {
257        Self { items, next_key }
258    }
259}
260
261/// Input for paginated domain listing
262#[derive(CandidType, Clone, Deserialize, Serialize, Debug)]
263pub struct ListDomainsPageInput {
264    /// Optional starting point for pagination (domain name to start from)
265    pub start_key: Option<String>,
266    /// Maximum number of items to return per page
267    pub limit: Option<u32>,
268}
269
270impl ListDomainsPageInput {
271    pub fn new() -> Self {
272        Self {
273            start_key: None,
274            limit: None,
275        }
276    }
277}
278
279impl Default for ListDomainsPageInput {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285#[derive(CandidType, Deserialize, Serialize, Debug, Clone, Error)]
286pub enum ListDomainsPageError {
287    #[error("Unauthorized")]
288    Unauthorized,
289    #[error("Internal error: {0}")]
290    InternalError(String),
291}
292
293#[derive(CandidType, Deserialize, Serialize, Debug, Clone, Error)]
294pub enum GetLastChangeTimeError {
295    #[error("Unauthorized")]
296    Unauthorized,
297    #[error("Internal error: {0}")]
298    InternalError(String),
299}
300
301#[derive(CandidType, Deserialize, Serialize, Debug, Clone, IntoStaticStr, Error)]
302#[strum(serialize_all = "snake_case")]
303pub enum FetchTaskError {
304    #[error("Unauthorized")]
305    Unauthorized,
306    #[error("Internal error: {0}")]
307    InternalError(String),
308}
309
310#[derive(CandidType, Deserialize, Serialize, Debug, Clone, Error)]
311pub enum GetDomainStatusError {
312    #[error("Unauthorized")]
313    Unauthorized,
314    #[error("Internal error: {0}")]
315    InternalError(String),
316}
317
318#[derive(CandidType, Deserialize, Serialize, Debug, Clone, Error)]
319pub enum GetDomainEntryError {
320    #[error("Unauthorized")]
321    Unauthorized,
322    #[error("Internal error: {0}")]
323    InternalError(String),
324}
325
326#[derive(CandidType, Deserialize, Serialize, Debug, Clone, Error)]
327pub enum ListCertificatesPageError {
328    #[error("Unauthorized")]
329    Unauthorized,
330    #[error("Internal error: {0}")]
331    InternalError(String),
332}
333
334#[derive(CandidType, Deserialize, Serialize, Debug, Clone, IntoStaticStr, Error, PartialEq, Eq)]
335#[strum(serialize_all = "snake_case")]
336pub enum SubmitTaskError {
337    #[error("Unauthorized")]
338    Unauthorized,
339    #[error("Domain not found: {0}")]
340    DomainNotFound(String),
341    #[error("A non-existing task was submitted: {0}")]
342    NonExistingTaskSubmitted(TaskId),
343    #[error("Internal error: {0}")]
344    InternalError(String),
345}
346
347#[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Error)]
348pub enum HasNextTaskError {
349    #[error("Unauthorized")]
350    Unauthorized,
351    #[error("Internal error: {0}")]
352    InternalError(String),
353}
354
355#[derive(CandidType, Deserialize, Serialize, Debug, Clone, IntoStaticStr, Error)]
356#[strum(serialize_all = "snake_case")]
357pub enum TryAddTaskError {
358    #[error("Unauthorized")]
359    Unauthorized,
360    #[error("Domain not found: {0}")]
361    DomainNotFound(String),
362    #[error("Another task is already in progress for domain: {0}")]
363    AnotherTaskInProgress(String),
364    #[error("Certificate already issued for domain: {0}")]
365    CertificateAlreadyIssued(String),
366    #[error("Update requires an existing certificate: {0}")]
367    MissingCertificateForUpdate(String),
368    #[error("Internal error: {0}")]
369    InternalError(String),
370}