@haakco/hostingctl 0.0.0-stage → 1.0.310
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +658 -2
- package/bin/hostingctl.js +142 -0
- package/bin/platforms.js +70 -0
- package/dist/main.js +36954 -0
- package/dist/main.js.map +1 -0
- package/package.json +72 -4
package/README.md
CHANGED
|
@@ -1,3 +1,659 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hostingctl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The hosting platform's API from a terminal, for a client's laptop and an operator's — one binary, one
|
|
4
|
+
token, the same commands. It is **standalone**: no checkout, no database, no cluster credentials and no
|
|
5
|
+
Infisical. It speaks to the public API over HTTPS with a Bearer token, exactly as the web app does, and it
|
|
6
|
+
generates its own client from `api/openapi.json`, so its request and response types are the contract's own.
|
|
7
|
+
|
|
8
|
+
Generation does **not** decide which operations have a command — a generated function nobody calls is a
|
|
9
|
+
feature nobody can use. That accounting lives in `src/commands/operation-map.ts`, which names every published
|
|
10
|
+
operation and either the route that reaches it, a shared read, a protocol consumer, or the one policy conflict
|
|
11
|
+
with no route; `just cli-test` fails when an operation is unaccounted for or a mapped route does not exist.
|
|
12
|
+
|
|
13
|
+
All **370** published operations are accounted for: **362** are reachable from a command, 6 are protocol
|
|
14
|
+
consumers the API calls rather than a person (the asset reader, the broadcast authenticator, the state
|
|
15
|
+
manager's intent resolver, the webhook client, the GitHub webhook and first-party authorization), 1 is the
|
|
16
|
+
company read the `--company` resolver shares, and 1 is deployment rollback, which stays open because the
|
|
17
|
+
platform never downgrades a live release.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @haakco/hostingctl --version # no install at all
|
|
23
|
+
npm i -g @haakco/hostingctl # install, once
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Node 22 or newer runs the CLI, and nothing else — no checkout, no database, no cluster credentials. The
|
|
27
|
+
package also publishes a self-contained executable per platform (`@haakco/hostingctl-linux-x64` and its
|
|
28
|
+
four siblings, `@haakco/hostingctl-linux-arm64`, `@haakco/hostingctl-darwin-arm64`,
|
|
29
|
+
`@haakco/hostingctl-darwin-x64` and `@haakco/hostingctl-windows-x64`); that package's `bin` **is** the
|
|
30
|
+
native binary, so a machine with no Node installs the platform package directly and needs no runtime.
|
|
31
|
+
|
|
32
|
+
`hostingctl update` brings a copy to the published version: it detects how the copy was installed,
|
|
33
|
+
prints the command it will run, runs it, and reads the installed version again to confirm the change.
|
|
34
|
+
`hostingctl update --check` reports the published version and that command without changing anything.
|
|
35
|
+
For a global npm install the command is `npm i -g @haakco/hostingctl@latest`.
|
|
36
|
+
|
|
37
|
+
`hostingctl --version` reports the version of the copy that is running; a development build reports
|
|
38
|
+
`0.0.0-dev`.
|
|
39
|
+
|
|
40
|
+
## Authenticate
|
|
41
|
+
|
|
42
|
+
**A client** signs in with the platform's **authorization-code/PKCE** flow. The CLI keeps the session in a
|
|
43
|
+
**mode-0600 credential file** at `~/.config/hostingctl/credentials.json`, inside a 0700 directory, and checks
|
|
44
|
+
that mode on read — a wider file is refused rather than read. Access tokens last 60 minutes; the refresh token
|
|
45
|
+
owns the lifetime and rotation is the mechanism, so a command spends the stored session and renews the access
|
|
46
|
+
token silently rather than holding a long-lived credential. The protocol, its rules and the reasons for them
|
|
47
|
+
are owned by
|
|
48
|
+
[authentication patterns](../docs/development/authentication_patterns.md) — this README does not restate
|
|
49
|
+
them.
|
|
50
|
+
|
|
51
|
+
**For local work**, use the development fixtures owned by
|
|
52
|
+
[test users](../docs/development/test_users.md): they exist only in local, `DevelopmentSeeder` creates
|
|
53
|
+
them, and their password is public development data rather than a credential.
|
|
54
|
+
|
|
55
|
+
Sign in with the public client's registered ID:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
hostingctl login --api-url https://api.example.com/api/v1 --client-id <client-id>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The browser completes sign-in and consent. Before the session is written, the API is asked who the new token
|
|
62
|
+
is (`GET /me`): a token Passport minted but the API will not act on is a failed sign-in, and the working session
|
|
63
|
+
it would have replaced is left alone.
|
|
64
|
+
|
|
65
|
+
Without `--token`, subsequent commands use that saved session. For automation, set `HOSTINGCTL_API_URL` and
|
|
66
|
+
`HOSTINGCTL_TOKEN` instead. The isolated local acceptance proved real browser consent and refresh-token
|
|
67
|
+
rotation on 2026-10-08.
|
|
68
|
+
|
|
69
|
+
### Named sessions
|
|
70
|
+
|
|
71
|
+
One credential file holds several sessions. Each is a profile:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
hostingctl login --profile staging --api-url https://api.staging.example.com/api/v1 --client-id <id>
|
|
75
|
+
hostingctl whoami --profile staging # or HOSTINGCTL_PROFILE=staging
|
|
76
|
+
hostingctl auth status # the session a command would use, and its expiry
|
|
77
|
+
hostingctl auth logout --profile staging # revoke at the API, then remove it here
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Signing in makes that profile the default; `--profile` or `HOSTINGCTL_PROFILE` selects another. A file in the
|
|
81
|
+
older single-session shape is read as the `default` profile and rewritten in the profile shape on the next
|
|
82
|
+
save, so no one has to migrate by hand. A session is **bound to the target it was issued for**: `--api-url`
|
|
83
|
+
naming a different host is refused before any request rather than spending the token there.
|
|
84
|
+
|
|
85
|
+
### A machine with no browser
|
|
86
|
+
|
|
87
|
+
`--no-browser` prints the authorize URL, then reads the callback URL pasted on stdin. This is the case where
|
|
88
|
+
the browser runs on another machine and cannot reach this host's `127.0.0.1:8477` listener. Only the exact
|
|
89
|
+
registered redirect URI is accepted back, the state is compared, and neither the pasted URL nor its code is
|
|
90
|
+
ever echoed. The port is fixed because Passport matches redirect URIs exactly; if `8477` is in use the CLI
|
|
91
|
+
says which port to free instead of silently choosing another one.
|
|
92
|
+
|
|
93
|
+
### The account and impersonation
|
|
94
|
+
|
|
95
|
+
`auth` also covers the account itself: `profile`, `password`, `register`, `forgot-password`,
|
|
96
|
+
`reset-password`, `verify-email`, `closure show|request`, `picture set|remove` and `notifications`
|
|
97
|
+
(including `read <id>` and `read-all`). Every secret comes from `--<name>-file <path>` or `--<name>-stdin`,
|
|
98
|
+
never from an argument, and is never printed. `closure request` re-reads the review and sends its digest, so a
|
|
99
|
+
decision made against a different review is refused rather than confirmed.
|
|
100
|
+
|
|
101
|
+
`auth impersonate start --target <user-uuid> --company <slug> --reason <text> --store-as <profile>` writes the
|
|
102
|
+
impersonation token to **its own profile**. The operator's session is never replaced and is not made non-default;
|
|
103
|
+
act as the target with `--profile <name>`, and `auth impersonate stop` ends it and removes that profile.
|
|
104
|
+
|
|
105
|
+
Repository operators can reuse the approved in-process login without copying a token:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
just deploy-auth https://hosting-staging.haakdev.com OPERATOR_HOME_STAGING dev cli whoami --json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The helper supplies the CLI's documented environment variables, refuses URL/token overrides and
|
|
112
|
+
preserves its exit status. Build the internal artifact with `pnpm --filter @haakco/hostingctl build`
|
|
113
|
+
first. This adds no Infisical dependency to the standalone CLI. The credential still has only its
|
|
114
|
+
own company authority; CLI writes require approval and `--confirm`.
|
|
115
|
+
|
|
116
|
+
`src/live.integration.test.ts` is the real call, gated on the environment:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
HOSTINGCTL_API_URL=http://localhost:8000/api/v1 HOSTINGCTL_TOKEN=<token> \
|
|
120
|
+
pnpm --filter @haakco/hostingctl test
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Usage
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
hostingctl whoami Which account the token belongs to, and what it may do.
|
|
127
|
+
hostingctl whoami --json The same answer using the API's own field names.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Every command takes `--json`, and automation should prefer it: the field names are the API's, never a
|
|
131
|
+
second naming invented here. Human-readable output is the default.
|
|
132
|
+
|
|
133
|
+
Read the private supplier traces for a domain with:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
hostingctl registrar history <domain-uuid> --company acme --json
|
|
137
|
+
hostingctl registrar history <domain-uuid> --company acme --page 2 --json
|
|
138
|
+
hostingctl registrar supersede-transfer <domain-uuid> --company acme --operation <operation-uuid> --accept-terms --dry-run
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This requires domain management permission and returns up to 100 entries from the preceding month.
|
|
142
|
+
The API decrypts request and response bodies for this explicit read; the output can contain customer
|
|
143
|
+
contact details and should be handled as sensitive evidence.
|
|
144
|
+
The command prints a `--page` hint when older entries remain.
|
|
145
|
+
|
|
146
|
+
### The command surface
|
|
147
|
+
|
|
148
|
+
`hostingctl --help` lists every top-level command, and `<command> --help` prints that command's own groups,
|
|
149
|
+
its flags and what each one does. The whole surface, with every flag, is in the
|
|
150
|
+
[command reference](https://hosting.haak.co/docs/cli-reference), which is generated from the CLI's own
|
|
151
|
+
metadata and compared against it in CI. The groups, in one line each:
|
|
152
|
+
|
|
153
|
+
| Command | Covers |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `whoami`, `environments`, `system`, `diagnostics` | The session's identity and reach, every environment in a company, what the cluster still holds, and the platform's health answer |
|
|
156
|
+
| `company` | The company itself, its members and roles, its activity, its logo and favicon, and the invitation journey |
|
|
157
|
+
| `environment` | Create, read, scale, retry; profile coverage, domains and redirects, databases and exports, configuration and secrets, services, site access, shared runtime and application, tools, tuning, retention, portable export, and the release bundle |
|
|
158
|
+
| `application` | Create, rename, remove; build and deploy, build logs, runs and run logs, process schedules; recovery review and confirm; the HTTP cache and path routes; source binding; site access; the WordPress import and URL repair; the administrator credential |
|
|
159
|
+
| `access` | Who may reach one environment: settings, grants, SSH keys, the shared password, and the per-user view |
|
|
160
|
+
| `site-user` | The accounts that sign in to a company's sites |
|
|
161
|
+
| `keys` | The account's personal SSH keys and where each is granted |
|
|
162
|
+
| `email` | The mail service, its names, mailboxes, aliases, sending holds, send-as grants, IMAP sync (including CSV), and the platform's deliverability monitoring |
|
|
163
|
+
| `mailbox` | Mail-client setup, the fallback recovery address and reset links for one mailbox |
|
|
164
|
+
| `dns` | A managed name's zone, its zone-file export, records, signing, handover, and the reviewed template import |
|
|
165
|
+
| `domain`, `registrar` | The names a company holds, their proofs and releases; the registrar's state, price, consent writes and supplier traces |
|
|
166
|
+
| `cancellation`, `deletion-lock`, `backups`, `logs`, `services` | Cancelling an environment, arming its deletion lock, its backup tracks, its retained history, and its service inventory |
|
|
167
|
+
| `realm`, `repository` | One environment's deployment realm; the GitHub connections an application deploys from |
|
|
168
|
+
| `platform` | Platform-only work: domain claims and purge attestations, the client directory, company and account closure, alerts, the GitHub App, Yahoo feedback, and the prepared-content catalogue |
|
|
169
|
+
| `login`, `auth` | Signing in and out; the session's own reads, the account's settings, push tokens and impersonation |
|
|
170
|
+
|
|
171
|
+
Every write needs `--confirm`; `--dry-run` describes the change and sends nothing.
|
|
172
|
+
|
|
173
|
+
Options: `--api-url` (or `HOSTINGCTL_API_URL`) — the API base **including `/api/v1`**, which is
|
|
174
|
+
also the frontend's `VITE_API_URL` default — `--token` (or `HOSTINGCTL_TOKEN`), `--json`, `--help`.
|
|
175
|
+
|
|
176
|
+
Exit status: **0** ok, **1** refused by the API, **2** unusable — bad usage, no token, or an unreachable
|
|
177
|
+
API. A refusal prints the API's message, its actionable `detail` when it sent one, and its `X-Request-Id`,
|
|
178
|
+
which is what joins a failed command to the log line that explains it.
|
|
179
|
+
|
|
180
|
+
## Access users
|
|
181
|
+
|
|
182
|
+
Create a native login using an existing public SSH key file, not a private key or a password on argv:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
hostingctl access user add <uuid> --company acme --username acme_accept \
|
|
186
|
+
--name "Acme Accept" --public-key-file ~/.ssh/id_ed25519.pub --dry-run
|
|
187
|
+
hostingctl access user add <uuid> --company acme --username acme_accept \
|
|
188
|
+
--name "Acme Accept" --public-key-file ~/.ssh/id_ed25519.pub --confirm
|
|
189
|
+
hostingctl access users <uuid> --company acme --json
|
|
190
|
+
hostingctl access user remove <uuid> --company acme --access-user <user-uuid> --confirm
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Use the native user's `uuid` from the listing. Usernames are 3–32 lowercase letters, digits or underscores,
|
|
194
|
+
starting with a letter. `--key-name` overrides the key label, which defaults to `--name`. Missing, empty,
|
|
195
|
+
oversized and private-key files are refused before any request. Writes require `--confirm`;
|
|
196
|
+
`--dry-run` describes the change without sending it.
|
|
197
|
+
|
|
198
|
+
## Mailbox declaration import
|
|
199
|
+
|
|
200
|
+
`hostingctl mailbox import preview <email-domain-uuid> --company <slug> --format <csv|yaml> --file <path>`
|
|
201
|
+
reviews a bounded 64 KiB declaration through the company-authorized API. Replace `preview` with
|
|
202
|
+
`confirm` and add `--confirm` to create missing mailboxes; `--dry-run` sends no request. Confirmation
|
|
203
|
+
re-previews the same captured contents and submits its review token. Matching active mailboxes are
|
|
204
|
+
unchanged; conflicts refuse the batch, and accepted creation is not native readiness.
|
|
205
|
+
|
|
206
|
+
See the [CSV/YAML examples and versioned schema](../frontend/docs-site/docs/email.md#create-several-hosting-mailboxes).
|
|
207
|
+
Passwords, message copying and DNS changes are not part of this declaration.
|
|
208
|
+
|
|
209
|
+
## Mailbox setup and recovery
|
|
210
|
+
|
|
211
|
+
Read one mailbox's mail-client settings, hand them over, manage its fallback recovery address, and issue
|
|
212
|
+
reset links. These are the same `email.view`/`email.manage` operations the panel uses.
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
hostingctl mailbox setup <email-domain-uuid> <mailbox-uuid> --company acme
|
|
216
|
+
hostingctl mailbox instructions <email-domain-uuid> <mailbox-uuid> --company acme \
|
|
217
|
+
--recipient user@example.com --confirm
|
|
218
|
+
hostingctl mailbox fallback set <email-domain-uuid> <mailbox-uuid> --company acme \
|
|
219
|
+
--address backup@example.com --dry-run
|
|
220
|
+
hostingctl mailbox fallback remove <email-domain-uuid> <mailbox-uuid> --company acme --confirm
|
|
221
|
+
hostingctl mailbox recovery-link email <email-domain-uuid> <mailbox-uuid> --company acme \
|
|
222
|
+
--destination other@example.com --confirm
|
|
223
|
+
hostingctl mailbox recovery-link copy <email-domain-uuid> <mailbox-uuid> --company acme --confirm
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`setup` prints the mailbox address and username, the incoming IMAP and outgoing SMTP host, port and
|
|
227
|
+
security, the authentication requirement and the webmail URL — the deployment's named
|
|
228
|
+
`hosting.email.client_setup` values (`mail.hosting.haak.co` IMAP `993` TLS, SMTP `465` TLS, webmail
|
|
229
|
+
`webmail.hosting.haak.co/webmail`), never guessed from your domain or its MX records. Any part the
|
|
230
|
+
deployment left unconfigured prints `Unavailable`.
|
|
231
|
+
|
|
232
|
+
`fallback set` stores an address that stays **unconfirmed** until its verification link is completed;
|
|
233
|
+
`fallback remove`, and replacing it, revoke outstanding verification and recovery links. `instructions`
|
|
234
|
+
and `recovery-link email` go to `--recipient`/`--destination`, or the stored fallback when omitted, and
|
|
235
|
+
report the message as **queued**, not delivered. `recovery-link copy` prints the reset URL **once** with
|
|
236
|
+
its expiry and a sharing warning; anyone holding it can reset the mailbox password, and issuing any link
|
|
237
|
+
never changes the password. Every write needs `--confirm`; `--dry-run` describes the change without
|
|
238
|
+
sending it.
|
|
239
|
+
|
|
240
|
+
A mailbox owner recovering access needs no Hosting account:
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
hostingctl mailbox recovery request --address user@example.com --confirm
|
|
244
|
+
hostingctl mailbox recovery verify --token-file ~/token --confirm
|
|
245
|
+
hostingctl mailbox recovery reset --token-file ~/token --password-file ~/password --confirm
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
These three commands are the only ones that run **without a token or session**; point them at the API
|
|
249
|
+
with `--api-url` or `HOSTINGCTL_API_URL`. The recovery token and the new password are read from a
|
|
250
|
+
`--<name>-file` (a regular file no wider than mode 600) or `--<name>-stdin`, never from an argument, and
|
|
251
|
+
are never printed. `verify` confirms a fallback address; `reset` sets a new confirmed password and
|
|
252
|
+
reports that the change is recorded and applied in the background. A password reset does not change your
|
|
253
|
+
mailbox's separate application passwords or your active sessions.
|
|
254
|
+
|
|
255
|
+
## Platform domain decisions
|
|
256
|
+
|
|
257
|
+
System administrators confirm ownership of a name and move it between clients. These commands are
|
|
258
|
+
platform-only: a company-scoped or impersonated token is refused by the API, and the CLI reports that
|
|
259
|
+
refusal rather than working around it. Each acts on a domain claim by its own `uuid`.
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
hostingctl platform domain show <claim-uuid> --json
|
|
263
|
+
hostingctl platform domain attest <claim-uuid> --reason "the previous holder confirmed the move" --dry-run
|
|
264
|
+
hostingctl platform domain attest <claim-uuid> --reason "the previous holder confirmed the move" --confirm
|
|
265
|
+
hostingctl platform domain revoke <claim-uuid> --reason "the name was taken back" --confirm
|
|
266
|
+
hostingctl platform domain transfer <claim-uuid> --company beta --reason "client request" --confirm
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`show` reports the live ownership decision and the **recorded** routing observation separately, so neither
|
|
270
|
+
reads as the other. A transfer answers `pending` while the previous holder's routing withdrawal is only
|
|
271
|
+
asked for, and `completed` once the move is observed; the command prints which, and re-running `show`
|
|
272
|
+
watches it settle. Every decision needs a `--reason`; the three writes need `--confirm`, and `--dry-run`
|
|
273
|
+
describes the change without sending it. `--company` on `transfer` is the client the name moves to.
|
|
274
|
+
|
|
275
|
+
## Deployment realm lifecycle
|
|
276
|
+
|
|
277
|
+
Observe one environment's recorded deployment realm, adopt it, retry a failed convergence and record a
|
|
278
|
+
workload's audience name. This is the operator path for the environment's CustD realm boundary.
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
hostingctl realm show <environment-uuid> --company acme
|
|
282
|
+
hostingctl realm adopt <environment-uuid> --custd-company <uuid> --owning-user <uuid> \
|
|
283
|
+
--registration-policy <invite_only|open> --expected-revision <n> [--login-paused] \
|
|
284
|
+
[--audiences <path>] --dry-run
|
|
285
|
+
hostingctl realm retry <environment-uuid> --company acme --confirm
|
|
286
|
+
hostingctl realm audience <environment-uuid> <workload-uuid> --company acme --audience acme --confirm
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`show` prints what Hosting has recorded for that environment's realm — its state, expected and observed
|
|
290
|
+
revisions, whether logins are paused and whether the routes are held — read from the deployment identity the
|
|
291
|
+
access view already carries, so no new endpoint is added for it. **Recorded convergence is not provider
|
|
292
|
+
health, a working sign-in or a released route:** `ready` means the recorded intent converged, and a route
|
|
293
|
+
that is not held is not thereby proven live. `--json` carries the API's own deployment-identity object.
|
|
294
|
+
|
|
295
|
+
`adopt` records the environment's realm allocation intent, and is **platform-addressed**: the staff actor who
|
|
296
|
+
may attest a CustD company's authority belongs to no company, so the token needs the platform permission for
|
|
297
|
+
it. The authority is existing tenant identity only — the CustD company (`--custd-company`) and its real
|
|
298
|
+
owning user (`--owning-user`) — and no provider project, environment or directory identifier is ever an
|
|
299
|
+
input. `--registration-policy` is `invite_only` or `open`; `--expected-revision` is the revision the caller
|
|
300
|
+
reviewed; `--login-paused` records paused logins; and `--audiences` names a JSON file of audience
|
|
301
|
+
registrations in the API's own shape
|
|
302
|
+
(`[{"audience":"acme","publicClient":true,"redirectUris":["https://acme.test/callback"]}]`), which the API
|
|
303
|
+
validates rather than this CLI inventing registration settings. An omitted file means no recorded audience,
|
|
304
|
+
and the API refuses a list that does not match the environment's recorded workload audiences.
|
|
305
|
+
|
|
306
|
+
`retry` asks the API to reconcile the realm from its recorded intent: the accepted answer records the retry,
|
|
307
|
+
and reconciliation observes the result. `audience` records one application workload's realm audience name
|
|
308
|
+
(`--audience`), which the adopted intent then has to match. All three writes need `--confirm`; `--dry-run`
|
|
309
|
+
describes the change and sends nothing.
|
|
310
|
+
|
|
311
|
+
## Email service reads
|
|
312
|
+
|
|
313
|
+
Read the company's business-email service and the names, mailboxes and aliases attached to it. These
|
|
314
|
+
are the same `email.view` reads the panel uses; the CLI changes nothing here.
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
hostingctl email show --company acme
|
|
318
|
+
hostingctl email names --company acme --json
|
|
319
|
+
hostingctl email mailboxes <email-domain-uuid> --company acme
|
|
320
|
+
hostingctl email mailbox <email-domain-uuid> <mailbox-uuid> --company acme
|
|
321
|
+
hostingctl email aliases <email-domain-uuid> --company acme
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`show` reports the service state, its entitlement and whether enrollment is available. `names` reports
|
|
325
|
+
each attached name with the readiness checks the platform observed for it, kept apart from the desired
|
|
326
|
+
status: an attached name is not a name whose mail is proven. `mailboxes` and `aliases` follow every
|
|
327
|
+
page the API reports rather than truncating at the first, and `mailbox` reads one record independently
|
|
328
|
+
of index pagination.
|
|
329
|
+
|
|
330
|
+
## Mailbox import from CSV or YAML
|
|
331
|
+
|
|
332
|
+
Create many mailboxes at once on one attached name from a `localPart,quota` CSV or a versioned YAML
|
|
333
|
+
document. Uploading and previewing writes nothing; the confirmed import is `email.manage`, the same
|
|
334
|
+
capability as creating one mailbox by hand.
|
|
335
|
+
|
|
336
|
+
```text
|
|
337
|
+
hostingctl email mailbox-import preview <email-domain-uuid> --company acme --file mailboxes.csv
|
|
338
|
+
hostingctl email mailbox-import confirm <email-domain-uuid> --company acme --file mailboxes.yaml --dry-run
|
|
339
|
+
hostingctl email mailbox-import confirm <email-domain-uuid> --company acme --file mailboxes.yaml --confirm
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The `--file` is a `localPart,quota` CSV (`.csv`) or a versioned YAML document (`.yaml`/`.yml`), with the
|
|
343
|
+
same declarations in either format; the format is read from the file's extension and the file is read
|
|
344
|
+
from disk and never printed. `preview` classifies every declaration against the name and reports what
|
|
345
|
+
it would create, what already matches and what is refused, without writing anything. `confirm`
|
|
346
|
+
re-runs that review to obtain the digest the API requires, then creates the reviewed declarations. It
|
|
347
|
+
needs `--confirm`; `--dry-run` describes the batch without sending it. An import is create-only: a
|
|
348
|
+
repeat of matching declarations is a no-op, a conflicting declaration is a refusal, and a refused
|
|
349
|
+
batch creates nothing. Creating a mailbox does not deliver its password; each new mailbox receives its
|
|
350
|
+
credential through its own owner. Imports never configure source accounts, start a sync or change DNS.
|
|
351
|
+
|
|
352
|
+
## Portable site pattern
|
|
353
|
+
|
|
354
|
+
Export one authorized environment's supported setup as YAML, then preview and import it into another
|
|
355
|
+
environment you are authorized for. The pattern is portable setup intent with logical references, not
|
|
356
|
+
a clone: no secret value, source-company identifier, observed readiness, message or file leaves the
|
|
357
|
+
platform.
|
|
358
|
+
|
|
359
|
+
```text
|
|
360
|
+
hostingctl environment portable export <environment-uuid> --company acme
|
|
361
|
+
hostingctl environment portable export <environment-uuid> --company acme --selection selection.json --out acme.yaml
|
|
362
|
+
hostingctl environment portable preview <environment-uuid> --company acme --file acme.yaml
|
|
363
|
+
hostingctl environment portable preview <environment-uuid> --company acme --file acme.yaml --bindings bindings.json
|
|
364
|
+
hostingctl environment portable prepare <environment-uuid> --company acme --file acme.yaml --bindings bindings.json --dry-run
|
|
365
|
+
hostingctl environment portable prepare <environment-uuid> --company acme --file acme.yaml --bindings bindings.json --confirm
|
|
366
|
+
hostingctl environment portable confirm <environment-uuid> --company acme --file acme.yaml --bindings bindings.json --confirm
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`export` prints the pattern as YAML on stdout (pipe it, or use `--out <path>`). `--selection` is a JSON
|
|
370
|
+
file naming the sections (`hosting`, `domains`, `dns`, `email`, `access`, `sources`) and the linked names
|
|
371
|
+
to include; the API's own defaults apply when it is omitted, and any section it did not include is
|
|
372
|
+
reported on stderr so the document on stdout stays byte-identical.
|
|
373
|
+
|
|
374
|
+
`preview` classifies every declaration against the target and writes nothing; it reports each section
|
|
375
|
+
as create, reuse, conflict, unresolved, refused or unsupported, the declared secret names, the ones
|
|
376
|
+
still unset and any blockers. `--file` is the pattern and `--bindings` is a JSON file whose `bindings`
|
|
377
|
+
map the pattern's logical references to authorized target resources (never a name alone) and whose
|
|
378
|
+
`consents` grant the consequential operations a selected section needs. Neither file carries a secret
|
|
379
|
+
value.
|
|
380
|
+
|
|
381
|
+
The two files are small and explicit. A selection names what leaves the source:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"sections": ["hosting", "domains", "dns", "email", "access", "sources"],
|
|
386
|
+
"domains": ["shop.example.com"],
|
|
387
|
+
"emailDomains": ["example.com"],
|
|
388
|
+
"dns": ["example.com"],
|
|
389
|
+
"access": true,
|
|
390
|
+
"sources": true
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
A bindings file maps the pattern's own references to target identifiers and grants the consequential
|
|
395
|
+
operations. `loginKeys` carries the **public** half of a key a declared SFTP/SSH login needs, and a
|
|
396
|
+
consent left `false` keeps its operation refused:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"bindings": {
|
|
401
|
+
"applications": { "web": "<target-application-uuid>" },
|
|
402
|
+
"repositories": { "web": "<target-repository-selection-uuid>" },
|
|
403
|
+
"domains": { "shop.example.com": "<company-domain-uuid>", "example.com": "<company-domain-uuid>" },
|
|
404
|
+
"emailDomains": { "example.com": "<email-domain-uuid>" },
|
|
405
|
+
"services": { "cache": "<service-uuid>" },
|
|
406
|
+
"siteUsers": { "person@example.com": "<site-user-uuid>" },
|
|
407
|
+
"accessUsers": { "deploy": "<access-user-uuid>" },
|
|
408
|
+
"members": { "person@example.com": "<company-member-user-uuid>" },
|
|
409
|
+
"aliasRecipients": ["external@example.com"],
|
|
410
|
+
"loginKeys": { "deploy": "ssh-ed25519 AAAA... deploy@laptop" }
|
|
411
|
+
},
|
|
412
|
+
"consents": { "serviceAddition": false, "emailInvitations": false, "autoDeploy": false }
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
A binding always names a target that already exists: an existing workload, the repository selection
|
|
417
|
+
a source builds from (`repositories`), a company-owned website name and the customer DNS zone a
|
|
418
|
+
record set lives in (both `domains`), the attached email name (`emailDomains`), an existing access
|
|
419
|
+
login (`accessUsers`), an existing agency site user (`siteUsers`), the Company member a grant names
|
|
420
|
+
(`members`), and any alias destination the pattern does not itself declare (`aliasRecipients`). A
|
|
421
|
+
declaration that **creates** a resource — a new application, mailbox or login — needs no binding, and
|
|
422
|
+
a name that merely matches a target is never authorization.
|
|
423
|
+
|
|
424
|
+
`prepare` records a reviewed draft without applying resource changes. It may create binding metadata
|
|
425
|
+
and advance the configuration generation; this makes declared secret names writable. Set each unset
|
|
426
|
+
value through the existing write-only secret control, then `preview` and `confirm` again. `confirm`
|
|
427
|
+
re-checks current state, records create/reuse intent and queues normal reconciliation. It does not
|
|
428
|
+
purchase, cut over DNS or remove anything, and its answer is accepted intent rather than a readiness
|
|
429
|
+
claim. `prepare` and `confirm` need `--confirm`; `--dry-run` describes the change without sending it. A
|
|
430
|
+
target that changed since the review is refused by the API rather than applied against state nobody saw.
|
|
431
|
+
|
|
432
|
+
## Managed DNS and import
|
|
433
|
+
|
|
434
|
+
Inspect a managed name's zone and its standard zone-file export, and review then apply an import. The
|
|
435
|
+
reads use `dns.view`; `dns import` is `dns.manage`.
|
|
436
|
+
|
|
437
|
+
```text
|
|
438
|
+
hostingctl dns zone <domain-uuid> --company acme
|
|
439
|
+
hostingctl dns export <domain-uuid> --company acme --json
|
|
440
|
+
hostingctl dns import preview <domain-uuid> --company acme --file records.txt
|
|
441
|
+
hostingctl dns import apply <domain-uuid> --company acme --file records.txt --dry-run
|
|
442
|
+
hostingctl dns import apply <domain-uuid> --company acme --file records.txt --confirm
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`zone` reports the published record sets, the desired generation and the last generation observed
|
|
446
|
+
applied; `export` prints a heading and then the zone file the API rendered (its `--json` carries the
|
|
447
|
+
file alone in `zoneText` for piping). The import `--file` holds one record set per
|
|
448
|
+
line as `name TTL type value` — the grammar the panel's import box uses — with blank lines and `#`
|
|
449
|
+
comments skipped and repeated `name/type/TTL` lines folded into one set with several values. The file
|
|
450
|
+
is read from disk and never printed.
|
|
451
|
+
|
|
452
|
+
`preview` classifies every submitted set and writes nothing. `apply` re-runs that review to obtain the
|
|
453
|
+
generation and review token the API requires, then records the reviewed sets and queues the apply
|
|
454
|
+
pass, so an apply is desired state rather than an observed completion. It needs `--confirm`; `--dry-run`
|
|
455
|
+
describes the change without sending it. A zone that changed since the review is refused by the API
|
|
456
|
+
rather than applied against a state nobody saw. There is no `dnssec` read: DNSSEC is a write-only route
|
|
457
|
+
in the API, and the observed mode is part of `dns zone`.
|
|
458
|
+
|
|
459
|
+
## Creating an environment
|
|
460
|
+
|
|
461
|
+
Create an environment from a package the platform sells. The CLI reads the package catalog from the API,
|
|
462
|
+
so it offers exactly the packages the platform offers, and it never names a cluster: placement belongs to
|
|
463
|
+
the platform and the create request carries no cluster field.
|
|
464
|
+
|
|
465
|
+
```text
|
|
466
|
+
hostingctl environment create --company acme --name "Acme Shop" --dry-run
|
|
467
|
+
hostingctl environment create --company acme --name "Acme Shop" --slug acme-shop --package small_sites --confirm
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`--name` is the environment's display name. `--slug` is the name it answers on: omitted, the CLI derives one
|
|
471
|
+
from the name (lowercase, each run of other characters folded to a single hyphen, trimmed, capped at 63
|
|
472
|
+
characters), and a slug shorter than three characters or outside lowercase letters, digits and single
|
|
473
|
+
hyphens that start and end with a letter or digit is refused before any request. `--package` names one of the
|
|
474
|
+
catalog's keys and defaults to its first; a key the catalog does not offer is refused before anything is
|
|
475
|
+
sent.
|
|
476
|
+
|
|
477
|
+
The write needs `--confirm`; `--dry-run` describes it, reads the catalog so the package it names is the one
|
|
478
|
+
the API sells, and sends nothing. The accepted answer is recorded desired state rather than a ready
|
|
479
|
+
environment: reconciliation applies it afterwards, and `--json` carries the API's own created environment
|
|
480
|
+
object.
|
|
481
|
+
|
|
482
|
+
## Environment databases
|
|
483
|
+
|
|
484
|
+
Read an environment's own SQL identities, declare one, replace who may consume it, and retire one while
|
|
485
|
+
keeping its data and its backups. These are the same environment-owned databases the configuration review
|
|
486
|
+
shows; no password is read and no credential is issued here.
|
|
487
|
+
|
|
488
|
+
```text
|
|
489
|
+
hostingctl environment databases <environment-uuid> --company acme
|
|
490
|
+
hostingctl environment database create <environment-uuid> --company acme \
|
|
491
|
+
--key shop --engine mariadb --dry-run
|
|
492
|
+
hostingctl environment database bind <environment-uuid> <database-uuid> --company acme \
|
|
493
|
+
--bind <workload-uuid>[,<workload-uuid>] --confirm
|
|
494
|
+
hostingctl environment database bind <environment-uuid> <database-uuid> --company acme --clear-bindings --confirm
|
|
495
|
+
hostingctl environment database review <environment-uuid> <database-uuid> --company acme
|
|
496
|
+
hostingctl environment database retire <environment-uuid> <database-uuid> --company acme --dry-run
|
|
497
|
+
hostingctl environment database retire <environment-uuid> <database-uuid> --company acme \
|
|
498
|
+
--digest <review-digest> --generation <review-generation> --confirm
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`databases` lists each identity with its engine, its configuration key, the applications allowed to consume
|
|
502
|
+
it and any recorded retirement, and carries the API's `canManage` rather than assuming the account may
|
|
503
|
+
write. `create` declares one independent, environment-owned database: `--key` is the stable declaration key
|
|
504
|
+
and `--engine` is `mariadb`, `postgres` or `mysql`; `--bind` grants it to consuming workloads as it is made,
|
|
505
|
+
and omitting it creates a real zero-consumer database. The API owns the key grammar and refuses a bad one.
|
|
506
|
+
|
|
507
|
+
`bind` replaces the consumer list. `--bind <uuid>[,<uuid>…]` names the new list and `--clear-bindings`
|
|
508
|
+
detaches every application; one is required and they cannot be combined, so an omitted list never clears a
|
|
509
|
+
grant by accident. Detaching stops future credential delivery only — a password an application already holds
|
|
510
|
+
is not revoked — and the shared service refuses to detach a consumer until that workload is stopped and its
|
|
511
|
+
environment generation has reconciled.
|
|
512
|
+
|
|
513
|
+
`review` reads what retiring one identity would do, what it retains, and the digest and generation a confirmation
|
|
514
|
+
carries back. It reports the API's own `blockedReason` when it cannot proceed (a consumer is still attached, a
|
|
515
|
+
retirement is already open, the environment is cancelled). Copy the digest and generation from that review to
|
|
516
|
+
`retire`; the CLI sends those exact values, and the API refuses them if the inventory or generation changed.
|
|
517
|
+
Recording the request is **accepted work, not a
|
|
518
|
+
completed retirement**: it reports `pending` (or `failed`, which can be reviewed and requested again) until
|
|
519
|
+
a reconcile has observed the SQL server refusing new logins. New logins then stop working; existing SQL
|
|
520
|
+
sessions are **not** terminated and the applications using the database are neither stopped nor unbound. The
|
|
521
|
+
database, every row, the database name, the SQL user name, the credential version and the backup track are
|
|
522
|
+
retained — there is no delete, and no command here removes a database or its data.
|
|
523
|
+
|
|
524
|
+
All three writes need `--confirm`. `--dry-run` sends no mutation: `create` and `bind` still read the
|
|
525
|
+
environment's current metadata so a stale change is refused rather than applied, while `retire` sends no
|
|
526
|
+
request at all — it does not even read the review.
|
|
527
|
+
|
|
528
|
+
## Environment configuration and secrets
|
|
529
|
+
|
|
530
|
+
Read an environment's desired state, review a YAML document against it, record a draft or apply it, and set
|
|
531
|
+
the declared secret values it needs. These are the same routes the panel uses; no value is ever printed.
|
|
532
|
+
|
|
533
|
+
```text
|
|
534
|
+
hostingctl environment configuration show <environment-uuid> --company acme
|
|
535
|
+
hostingctl environment configuration preview <environment-uuid> --company acme --file acme.yaml
|
|
536
|
+
hostingctl environment configuration save <environment-uuid> --company acme --file acme.yaml --confirm
|
|
537
|
+
hostingctl environment configuration apply <environment-uuid> --company acme --file acme.yaml --confirm
|
|
538
|
+
hostingctl environment configuration apply <environment-uuid> --company acme --file acme.yaml \
|
|
539
|
+
--application-uuid <existing-workload-uuid> --confirm
|
|
540
|
+
hostingctl environment secret write <environment-uuid> --company acme \
|
|
541
|
+
--key app_key --value-source file --value-file ~/app-key --confirm
|
|
542
|
+
hostingctl environment secret write <environment-uuid> --company acme \
|
|
543
|
+
--key app_key --value-source generate --confirm
|
|
544
|
+
hostingctl environment secret import <environment-uuid> --company acme --file secrets.env --confirm
|
|
545
|
+
hostingctl environment secret local <environment-uuid> --company acme --out .env.local
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`show` names the current, active and known-good revisions with each declared secret and whether it holds a
|
|
549
|
+
value. `preview` classifies the document and writes nothing; `--apply` reviews it as an apply so the verdict
|
|
550
|
+
is the one an apply relies on. `save` records a **draft** revision (not applied); `apply` records it and queues
|
|
551
|
+
normal reconciliation. Both re-run the review to obtain the hash and generation the API requires, so a
|
|
552
|
+
document that changed since review is refused rather than applied against state nobody saw.
|
|
553
|
+
For a document with one application, `--application-uuid` explicitly adopts an existing workload in that
|
|
554
|
+
environment. The same UUID is sent in preview and save so the reviewed hash covers the intended adoption.
|
|
555
|
+
|
|
556
|
+
A secret value is never taken from an argument. `--value-source file|stdin` reads it from a private file
|
|
557
|
+
(mode 600) or `--value-stdin`. `--value-source generate` is only for the declared secret a Laravel
|
|
558
|
+
application binds to its `APP_KEY` environment variable — the fixture's `app_key`, or a member-scoped
|
|
559
|
+
`<memberKey>_app_key` — 32 random bytes, base64-encoded and `base64:` prefixed. The secret's own name never
|
|
560
|
+
decides this: the binding in the reviewed document does, so generation refuses a declared secret that is
|
|
561
|
+
not bound to `APP_KEY`. It also refuses to replace a value already in use unless `--replace` says to rotate
|
|
562
|
+
it, because rotating it invalidates data encrypted with the old key. `import` sets several declared secrets
|
|
563
|
+
from a `KEY=VALUE` file (mode 600) and is how an existing key is brought over unchanged. A value is read only
|
|
564
|
+
when the change is sent, so a dry run reads nothing, and no value appears in output, logs or a dry-run
|
|
565
|
+
description. Every write needs `--confirm`; `--dry-run` describes the change and sends nothing.
|
|
566
|
+
|
|
567
|
+
`local` is the opposite direction and the one to use for a development machine. It reads nothing from the
|
|
568
|
+
platform's secret store: it writes each declared secret's own **local development value** — the `local` field
|
|
569
|
+
in the document, a harmless default such as `password` — into an env file, and names the declared secrets
|
|
570
|
+
that have none so you supply those yourself rather than discovering a blank variable at runtime. Because a
|
|
571
|
+
stored value never appears in the document, there is no path by which a production credential can reach that
|
|
572
|
+
file. It writes to `--out` (mode 600, since you may add a real value to the same file) or to stdout, and it
|
|
573
|
+
needs no `--confirm` because it changes nothing on the platform.
|
|
574
|
+
|
|
575
|
+
## Environment services
|
|
576
|
+
|
|
577
|
+
Read one environment's whole service inventory with the generation it was observed at:
|
|
578
|
+
|
|
579
|
+
```text
|
|
580
|
+
hostingctl services <environment-uuid> --company acme
|
|
581
|
+
hostingctl services <environment-uuid> --company acme --json
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The header names the desired and applied generation and the observed status in words — `nothing observed yet`
|
|
585
|
+
when the environment has never been observed, and `ready` with `applied M of N (change outstanding)` when
|
|
586
|
+
the live generation is behind the desired one. Each service follows with whether it is installed and
|
|
587
|
+
running, its replicas, its reserved allocation when it has one, and its consumers.
|
|
588
|
+
|
|
589
|
+
## Applications
|
|
590
|
+
|
|
591
|
+
One application's build, deployment, process runs, rename and removal:
|
|
592
|
+
|
|
593
|
+
```text
|
|
594
|
+
hostingctl application list <environment-uuid> --company acme
|
|
595
|
+
hostingctl application show <environment-uuid> <workload-uuid> --company acme
|
|
596
|
+
hostingctl application deployment <environment-uuid> <workload-uuid> --company acme
|
|
597
|
+
hostingctl application build <environment-uuid> <workload-uuid> --company acme --source <selection-uuid> --confirm
|
|
598
|
+
hostingctl application deploy <environment-uuid> <workload-uuid> <release-uuid> --company acme --confirm
|
|
599
|
+
hostingctl application runs <environment-uuid> <workload-uuid> --company acme
|
|
600
|
+
hostingctl application run-log <environment-uuid> <workload-uuid> <run-name> --company acme
|
|
601
|
+
hostingctl application schedule-run <environment-uuid> <workload-uuid> <schedule-key> --company acme --confirm
|
|
602
|
+
hostingctl application runtime <environment-uuid> <workload-uuid> --company acme --mode stop --confirm
|
|
603
|
+
hostingctl application update <environment-uuid> <workload-uuid> --company acme --label "Shop" --confirm
|
|
604
|
+
hostingctl application removal <environment-uuid> <workload-uuid> --company acme
|
|
605
|
+
hostingctl application remove <environment-uuid> <workload-uuid> --company acme --confirm
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
`deployment` reports the running, requested and interrupted builds and releases, the bound source and whether
|
|
609
|
+
a push would deploy. `build` queues a build of the application's applied configuration from `--source` (or the
|
|
610
|
+
bound selection), resolved to an exact commit by the source service rather than named here. `deploy` makes an
|
|
611
|
+
existing release the one the application runs. `runs` and `run-log` read the process run history and one run's
|
|
612
|
+
own output, which may report `unavailable` when the cluster cannot answer rather than an empty body.
|
|
613
|
+
`schedule-run` runs a **declared** schedule once, now, beside the schedule's own runs; it does not execute an
|
|
614
|
+
arbitrary command, for which the API has no route. `update` renames the label only, never the immutable name.
|
|
615
|
+
`runtime` records one application's desired runtime from `--mode stop|start|restart`: it reads the
|
|
616
|
+
environment's current generation and sends `running: false` for stop or `running: true` for start and
|
|
617
|
+
restart. Starting or restarting records a fresh runtime revision even if the application is already running,
|
|
618
|
+
and stopping is recorded as desired intent that reconciliation observes afterwards — the accepted answer is
|
|
619
|
+
not an observed stopped state. `--dry-run` makes no request at all, and a moved generation is the API's own
|
|
620
|
+
conflict rather than a silent refetch.
|
|
621
|
+
|
|
622
|
+
`removal` reviews what removing one application would affect; `remove` re-reads that review and sends its own
|
|
623
|
+
`inventoryDigest`, so a confirmation approves an exact inventory rather than a label and the environment
|
|
624
|
+
generation makes a stale review a conflict. Removal withdraws the runtime through normal reconciliation and
|
|
625
|
+
deletes no retained data; retiring an environment-owned database's data is a separate decision. Every write
|
|
626
|
+
needs `--confirm`; `--dry-run` describes the change and sends nothing.
|
|
627
|
+
|
|
628
|
+
## Retained log history
|
|
629
|
+
|
|
630
|
+
Read the platform's bounded history for one of your resources. This is retained, sanitized operational
|
|
631
|
+
history, not the live container tail:
|
|
632
|
+
|
|
633
|
+
```text
|
|
634
|
+
hostingctl logs environment <environment-uuid> --company acme
|
|
635
|
+
hostingctl logs dns <domain-uuid> --company acme --json
|
|
636
|
+
hostingctl logs mail <email-domain-uuid> --company acme
|
|
637
|
+
hostingctl logs environment <environment-uuid> --company acme \
|
|
638
|
+
--start 2026-10-14T11:00:00Z --end 2026-10-14T12:00:00Z --limit 100
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
`environment`, `dns` and `mail` take the environment, DNS domain and mail domain uuid. The API bounds
|
|
642
|
+
every read: at most a six-hour window within the last 24 hours, defaulting to the most recent hour, and
|
|
643
|
+
1–200 entries, defaulting to 100. `--start` and `--end` are ISO instants and `--limit` is a whole number;
|
|
644
|
+
the server enforces the window and the CLI refuses a malformed `--limit` before asking.
|
|
645
|
+
|
|
646
|
+
A `source` of `unavailable` means the platform could not read the retained history — no configured
|
|
647
|
+
reader, an active ownership interval that cannot be proven, or a boundary failure for an authorized
|
|
648
|
+
resource. An out-of-company UUID is refused before reading history. Unavailable is not the empty
|
|
649
|
+
result of a successful read that matched nothing; the human output distinguishes these states. The
|
|
650
|
+
environment family reads dedicated web-container output; DNS reports zone-transfer initiation, not full
|
|
651
|
+
query history; and mail reports a Postfix delivery attempt, which is transport acceptance rather than
|
|
652
|
+
proof of mailbox receipt.
|
|
653
|
+
|
|
654
|
+
## What it never does
|
|
655
|
+
|
|
656
|
+
No cluster or database access, ever. Those credentials belong to the platform, and a client's laptop is not
|
|
657
|
+
where they live. Any rule the CLI appears to need — what a complete resource profile is, which statuses
|
|
658
|
+
exist, who may see an environment — comes from the API's own answer, because a CLI that re-derives platform
|
|
659
|
+
rules becomes a second owner of them.
|