uarp_sdk/generated/api/tenants.rs
1// Code generated by @uarp/codegen from spec/openapi.json. DO NOT EDIT.
2//!
3//! Tenant management and API keys
4
5#![allow(unused_imports, clippy::too_many_arguments)]
6
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9
10use crate::client::{Client, Request, NO_BODY, NO_QUERY};
11use crate::error::Result;
12use crate::generated::models;
13use crate::multipart::{field_text, FilePart};
14use crate::util::encode_path;
15
16/// Tenant management and API keys
17#[derive(Debug, Clone)]
18pub struct TenantsApi {
19 pub(crate) client: Client,
20}
21
22impl Client {
23 /// Tenant management and API keys
24 pub fn tenants(&self) -> TenantsApi {
25 TenantsApi { client: self.clone() }
26 }
27}
28
29impl TenantsApi {
30 /// Accept a pending invite from the tenant picker
31 ///
32 /// The tenant is in the PATH, and that is the whole reason this route exists beside `POST
33 /// /api/v1/users/invites/{inviteId}/accept`. The older route resolves the invite against the
34 /// caller's active tenant, which cannot work here: the caller is not a member of the inviting
35 /// tenant yet — making them one is what the call is for. Without a tenant-in-path route the
36 /// picker's Accept button can never succeed for a cross-tenant invite.
37 ///
38 /// Semantics are otherwise identical to the older route: the same atomic create-user + email
39 /// index + membership index + `invite.status = accepted` write.
40 ///
41 /// Four refusals, and they are different questions: **403** the invite is addressed to another
42 /// email, or the `token` does not match; **409** the invite is not `pending` (already accepted,
43 /// revoked, declined) or the email already belongs to a member; **410** the invite has expired;
44 /// **404** no such invite, or the caller has no user record.
45 ///
46 /// `POST /api/v1/me/invites/{tenantId}/{inviteId}/accept`
47 pub async fn accept_invite_from_picker(&self, tenant_id: &str, invite_id: &str, body: &models::AcceptInviteFromPickerRequest) -> Result<models::AcceptInviteFromPickerResponse> {
48 self.client
49 .request_json(Request {
50 method: Method::POST,
51 path: format!("/api/v1/me/invites/{}/{}/accept", encode_path(tenant_id), encode_path(invite_id)),
52 query: NO_QUERY,
53 body: Some(body),
54 headers: Vec::new(),
55 idempotent: true,
56 })
57 .await
58 }
59
60 /// Create a new API key
61 ///
62 /// `POST /api/v1/tenants/me/keys`
63 ///
64 /// Required scopes: `tenants:write`.
65 pub async fn create_api_key(&self, body: &models::CreateAPIKeyRequest) -> Result<models::APIKeyResponse> {
66 self.client
67 .request_json(Request {
68 method: Method::POST,
69 path: "/api/v1/tenants/me/keys".to_string(),
70 query: NO_QUERY,
71 body: Some(body),
72 headers: Vec::new(),
73 idempotent: true,
74 })
75 .await
76 }
77
78 /// Create a new tenant with the caller as owner
79 ///
80 /// Provisions a brand-new tenant on the `free` plan (or `enterprise` for super-admin). The
81 /// caller becomes the founding owner. Onboarding wizard auto-runs on /browser to provision the
82 /// Head Agent.
83 ///
84 /// `POST /api/v1/me/tenants/create`
85 pub async fn create_my_tenant(&self, body: &models::CreateMyTenantRequest) -> Result<models::CreateMyTenantResponse> {
86 self.client
87 .request_json(Request {
88 method: Method::POST,
89 path: "/api/v1/me/tenants/create".to_string(),
90 query: NO_QUERY,
91 body: Some(body),
92 headers: Vec::new(),
93 idempotent: true,
94 })
95 .await
96 }
97
98 /// Decline a pending invite
99 ///
100 /// Deliberately distinct from an admin's revoke, so the audit trail and the Members page can
101 /// tell "the invitee said no" from "an admin pulled it". The caller's email must match the
102 /// invite's — without that check anyone with a current session could decline someone else's
103 /// invites.
104 ///
105 /// No token is required here, unlike accept: declining grants nothing.
106 ///
107 /// `POST /api/v1/me/invites/{tenantId}/{inviteId}/decline`
108 pub async fn decline_invite_from_picker(&self, tenant_id: &str, invite_id: &str) -> Result<models::DeclineInviteFromPickerResponse> {
109 self.client
110 .request_json(Request {
111 method: Method::POST,
112 path: format!("/api/v1/me/invites/{}/{}/decline", encode_path(tenant_id), encode_path(invite_id)),
113 query: NO_QUERY,
114 body: NO_BODY,
115 headers: Vec::new(),
116 idempotent: true,
117 })
118 .await
119 }
120
121 /// Get current tenant
122 ///
123 /// Returns the calling tenant's profile. Any authenticated key passes — no scope required.
124 ///
125 /// `GET /api/v1/tenants/me`
126 ///
127 /// Required scopes: `api_keys:read`.
128 pub async fn get_current_tenant(&self) -> Result<models::Tenant> {
129 self.client
130 .request_json(Request {
131 method: Method::GET,
132 path: "/api/v1/tenants/me".to_string(),
133 query: NO_QUERY,
134 body: NO_BODY,
135 headers: Vec::new(),
136 idempotent: false,
137 })
138 .await
139 }
140
141 /// Tiered Head Agent template for the onboarding wizard
142 ///
143 /// Returns Basic/Standard/Full tier metadata for the current tenant under the Variant-A
144 /// SPEC-driven contract. Each tier names the `@platform/*` SPECs the wizard installs (via
145 /// `install_specs`) plus an `auto_approve_tools` HITL list. Availability is a plan-rank check
146 /// (`required_plan`); locked tiers ship empty `install_specs`.
147 ///
148 /// `GET /api/v1/me/head-agent-template`
149 pub async fn get_my_head_agent_template(&self) -> Result<models::GetMyHeadAgentTemplateResponse> {
150 self.client
151 .request_json(Request {
152 method: Method::GET,
153 path: "/api/v1/me/head-agent-template".to_string(),
154 query: NO_QUERY,
155 body: NO_BODY,
156 headers: Vec::new(),
157 idempotent: false,
158 })
159 .await
160 }
161
162 /// DNS and certificate state for this tenant's custom domain
163 ///
164 /// **Two different 404s, and a client should tell them apart:** no such tenant, and a tenant
165 /// with no custom domain configured. The second is the ordinary state of most tenants and is
166 /// not an error condition — a UI that renders both as a failure will report a fault to every
167 /// customer who has not set up a vanity domain.
168 ///
169 /// Records written before the lifecycle schema are lifted on read, so `dns` and `cert` are
170 /// present here even for a domain added under the old flat fields.
171 ///
172 /// `GET /api/v1/tenants/me/domain/health`
173 ///
174 /// Required scopes: `api_keys:read`.
175 pub async fn get_tenant_domain_health(&self) -> Result<models::GetTenantDomainHealthResponse> {
176 self.client
177 .request_json(Request {
178 method: Method::GET,
179 path: "/api/v1/tenants/me/domain/health".to_string(),
180 query: NO_QUERY,
181 body: NO_BODY,
182 headers: Vec::new(),
183 idempotent: false,
184 })
185 .await
186 }
187
188 /// Leave a tenant
189 ///
190 /// Removes the caller's own membership. The user-record cascade matches an admin-driven
191 /// removal.
192 ///
193 /// Two refusals, both **409**, and both name a specific thing to do first. The caller is the
194 /// only ACTIVE owner: transfer ownership before leaving. Or the caller is the only veto-holding
195 /// ambassador: rotate the founder ambassador via `/api/v1/governance/ambassadors` first. The
196 /// second check is skipped entirely when governance is not enabled, so its absence is not a
197 /// promise that no such constraint exists.
198 ///
199 /// `DELETE /api/v1/me/memberships/{tenantId}`
200 pub async fn leave_tenant(&self, tenant_id: &str) -> Result<models::LeaveTenantResponse> {
201 self.client
202 .request_json(Request {
203 method: Method::DELETE,
204 path: format!("/api/v1/me/memberships/{}", encode_path(tenant_id)),
205 query: NO_QUERY,
206 body: NO_BODY,
207 headers: Vec::new(),
208 idempotent: true,
209 })
210 .await
211 }
212
213 /// List API keys
214 ///
215 /// `GET /api/v1/tenants/me/keys`
216 ///
217 /// Required scopes: `api_keys:read`.
218 pub async fn list_api_keys(&self) -> Result<models::ListAPIKeysResponse> {
219 self.client
220 .request_json(Request {
221 method: Method::GET,
222 path: "/api/v1/tenants/me/keys".to_string(),
223 query: NO_QUERY,
224 body: NO_BODY,
225 headers: Vec::new(),
226 idempotent: false,
227 })
228 .await
229 }
230
231 /// List the caller's tenant memberships + pending invites
232 ///
233 /// Returns every tenant the calling user is a member of, plus any pending invites addressed to
234 /// their email. Used by the Netflix-style picker (/browser/tenants).
235 ///
236 /// `GET /api/v1/me/tenants`
237 pub async fn list_my_tenants(&self) -> Result<models::ListMyTenantsResponse> {
238 self.client
239 .request_json(Request {
240 method: Method::GET,
241 path: "/api/v1/me/tenants".to_string(),
242 query: NO_QUERY,
243 body: NO_BODY,
244 headers: Vec::new(),
245 idempotent: false,
246 })
247 .await
248 }
249
250 /// Find which tenant owns the given agent across caller's memberships
251 ///
252 /// Used by the chat layout fallback when the URL points at an agent under a different active
253 /// tenant. Returns the owning tenant_id in one round-trip; the alternative was N client-side
254 /// probes that surfaced expected 404s in the browser console.
255 ///
256 /// `GET /api/v1/me/agents/{agentId}/locate`
257 pub async fn locate_my_agent(&self, agent_id: &str) -> Result<models::LocateMyAgentResponse> {
258 self.client
259 .request_json(Request {
260 method: Method::GET,
261 path: format!("/api/v1/me/agents/{}/locate", encode_path(agent_id)),
262 query: NO_QUERY,
263 body: NO_BODY,
264 headers: Vec::new(),
265 idempotent: false,
266 })
267 .await
268 }
269
270 /// Partial update tenant
271 ///
272 /// Partial update of tenant settings. Requires the `tenants:write` scope and role `owner`.
273 ///
274 /// `PATCH /api/v1/tenants/me`
275 ///
276 /// Required scopes: `tenants:write`.
277 pub async fn patch(&self, body: &serde_json::Map<String, serde_json::Value>) -> Result<serde_json::Map<String, serde_json::Value>> {
278 self.client
279 .request_json(Request {
280 method: Method::PATCH,
281 path: "/api/v1/tenants/me".to_string(),
282 query: NO_QUERY,
283 body: Some(body),
284 headers: Vec::new(),
285 idempotent: true,
286 })
287 .await
288 }
289
290 /// Revoke an API key
291 ///
292 /// `DELETE /api/v1/tenants/me/keys/{keyId}`
293 ///
294 /// Required scopes: `tenants:write`.
295 pub async fn revoke_api_key(&self, key_id: &str) -> Result<models::RevokeAPIKeyResponse> {
296 self.client
297 .request_json(Request {
298 method: Method::DELETE,
299 path: format!("/api/v1/tenants/me/keys/{}", encode_path(key_id)),
300 query: NO_QUERY,
301 body: NO_BODY,
302 headers: Vec::new(),
303 idempotent: true,
304 })
305 .await
306 }
307
308 /// Update current tenant settings
309 ///
310 /// `PUT /api/v1/tenants/me`
311 ///
312 /// Required scopes: `tenants:write`.
313 pub async fn update(&self, body: &models::UpdateTenantRequest) -> Result<serde_json::Value> {
314 self.client
315 .request_json(Request {
316 method: Method::PUT,
317 path: "/api/v1/tenants/me".to_string(),
318 query: NO_QUERY,
319 body: Some(body),
320 headers: Vec::new(),
321 idempotent: true,
322 })
323 .await
324 }
325
326 /// Verify domain
327 ///
328 /// `POST /api/v1/tenants/me/domain/verify`
329 ///
330 /// Required scopes: `tenants:write`.
331 pub async fn verify_tenant_domain(&self, body: &serde_json::Map<String, serde_json::Value>) -> Result<models::VerifyTenantDomainResponse> {
332 self.client
333 .request_json(Request {
334 method: Method::POST,
335 path: "/api/v1/tenants/me/domain/verify".to_string(),
336 query: NO_QUERY,
337 body: Some(body),
338 headers: Vec::new(),
339 idempotent: true,
340 })
341 .await
342 }
343}