@fabrika/provider-zerops 0.4.2 → 0.4.3

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.
Files changed (45) hide show
  1. package/package.json +8 -7
  2. package/src/api/client.ts +11 -14
  3. package/src/api/polling.ts +42 -0
  4. package/src/api/readers.ts +9 -2
  5. package/src/api/service-env-limit.ts +29 -0
  6. package/src/api/service-env-response.ts +34 -2
  7. package/src/api/types.ts +39 -9
  8. package/src/api.ts +10 -1
  9. package/src/control.ts +23 -8
  10. package/src/execution.ts +41 -3
  11. package/src/index.ts +5 -0
  12. package/src/installation/admin.ts +4 -2
  13. package/src/installation/checker-release.ts +44 -0
  14. package/src/installation/deploy.ts +66 -22
  15. package/src/installation/generated/platform-console-schema.ts +18 -2
  16. package/src/installation/generated/platform-proxy-manifest.ts +20 -0
  17. package/src/installation/guide.md +164 -52
  18. package/src/installation/index.ts +22 -15
  19. package/src/installation/init-options.ts +2 -2
  20. package/src/installation/init.ts +8 -2
  21. package/src/installation/install-collaborators.ts +4 -0
  22. package/src/installation/install-environment.ts +52 -30
  23. package/src/installation/install-options.ts +20 -19
  24. package/src/installation/install-project.ts +23 -15
  25. package/src/installation/install-provisioning.ts +12 -12
  26. package/src/installation/install-secrets.ts +89 -0
  27. package/src/installation/install-token.ts +37 -0
  28. package/src/installation/install.ts +34 -21
  29. package/src/installation/machine-key.ts +3 -2
  30. package/src/installation/manifest.ts +29 -12
  31. package/src/installation/platform-archive.ts +87 -0
  32. package/src/installation/platform-upload.ts +2 -2
  33. package/src/installation/secrets.ts +3 -4
  34. package/src/installation/topology/setups.ts +15 -27
  35. package/src/installation/topology/topology.ts +105 -104
  36. package/src/managed-env.ts +11 -0
  37. package/src/manifest.ts +25 -3
  38. package/src/namespace/codec.ts +6 -1
  39. package/src/namespace/errors.ts +3 -1
  40. package/src/namespace/provisioning.ts +18 -5
  41. package/src/namespace/proxy-attempt.ts +56 -14
  42. package/src/namespace/update.ts +50 -8
  43. package/src/provider.ts +57 -13
  44. package/src/proxy-manifest-carrier.ts +32 -0
  45. package/src/source/port.ts +9 -1
@@ -16,24 +16,72 @@ message that names the flag or the variable to set. Read the message: it names t
16
16
 
17
17
  **Ask the human for these first.** You cannot produce them, and each one blocks a later step:
18
18
 
19
- | What | Needed at |
20
- | ------------------------------------------------------------------------------ | ---------- |
21
- | A Zerops personal access token, generated in the Zerops GUI | step 1 |
22
- | A `gh` login allowed to create a repository in the organization that owns it | step 3 |
23
- | The administrator's e-mail, and the human to open the enrollment URL | step 2 |
24
- | For a PRIVATE application repository: the GitHub App connected in the console | step 5 |
25
- | The fabrika release tag to install, for example `v0.4.2` (a branch is refused) | steps 1, 3 |
19
+ | What | Needed at |
20
+ | ------------------------------------------------------------------------------------- | ---------- |
21
+ | A Zerops PERSONAL access token of a client OWNER, generated in the Zerops GUI | step 1 |
22
+ | A verified Zerops account (credit topped up once); an unverified one owns ONE project | step 5 |
23
+ | A `gh` login allowed to create a repository in the organization that owns it | step 3 |
24
+ | The administrator's e-mail address, and the human to open the enrollment URL | step 2 |
25
+ | For a PRIVATE application repository: the GitHub App connected in the console | step 5 |
26
+ | The fabrika release tag to install, for example `v0.4.3` (a branch is refused) | steps 1, 3 |
27
+ | The four decisions below | steps 1, 5 |
28
+
29
+ Every namespace is a Zerops project of its own, so the first one fails with `clientProjectCountLimitExceededTopUp`
30
+ on an unverified account (observed 2026-10-15).
26
31
 
27
32
  Also needed on the machine: [Bun](https://bun.sh) ≥ 1.3, `git`, `gh`, `curl` and `jq`.
28
33
 
34
+ ## Decisions: ask the human, with the prices
35
+
36
+ Each choice trades price against stability, and two of them cannot be undone. Show the human this section
37
+ and let them choose; do not pick for them. Prices are Zerops' list prices as of 2026-10-15
38
+ ([zerops.io/pricing](https://zerops.io/pricing)): a shared core $0.60, a dedicated core $6.00, RAM $3.00 per
39
+ GB, disk $0.10 per GB and object storage $0.01 per GB, each per 30 days and billed per minute. A Lightweight
40
+ project core is free (15 build hours, 100 GB egress); a Serious one is $10 (150 build hours, 3 TB egress, a
41
+ highly available balancer). The figures below are the idle FLOOR. Every service scales up under load, to 8
42
+ cores and 48 GB per container, and costs more while it does.
43
+
44
+ **1. Platform tier** (`platform install --tier`, step 1). Cannot be changed later: a project's core package
45
+ is upgrade-only, and the data services differ.
46
+
47
+ | Tier | What runs | Idle floor | Fails when |
48
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------- |
49
+ | `light` (default) | Lightweight core. One single-node PostgreSQL (`oltp-hobby`) and one bucket shared by IAM, control and Operations; one container each. | about $6 | any one container or the database is down: sign-in stops |
50
+ | `standard` | Serious core. HA PostgreSQL (3 nodes, `oltp-production`, dedicated CPU) for IAM and control; a second HA PostgreSQL and bucket for Operations; two containers of IAM and the proxy. | about $100 | survives the loss of one node of each |
51
+
52
+ Most of `standard`'s price is the identity database's dedicated floor (about $72). `light` is right for an
53
+ evaluation and a small fleet that tolerates an outage; `standard` for sign-in that must stay up.
54
+
55
+ **2. Namespace preset** (`control namespaces create --preset`, step 5). A namespace is the Zerops project
56
+ applications run in. Each one adds its own proxy, about $1.08.
57
+
58
+ | Preset | Where an application's database lives | Use with |
59
+ | --------------- | ----------------------------------------------------------------------- | ------------------------------------ |
60
+ | `cheap` | One shared single-node PostgreSQL in the namespace, about $1.45 for all | `fabrika.cheap.config.ts` |
61
+ | `mid` (default) | Each application imports its own database into the namespace | `fabrika.config.ts` |
62
+ | `full` | The namespace belongs to ONE application (`--exclusive-app=<id>`) | an application that must be isolated |
63
+
64
+ **3. The application's database** (the application's `fabrika.config.ts`, before step 5). The example
65
+ declares `postgresql:single@18` at `oltp-hobby`, about $1.45, plus its runtime from about $1.08 per
66
+ container. `postgresql:ha@18` survives a node failure: about $11 at `oltp-staging` (shared CPU), about $72 at
67
+ `oltp-production` (dedicated). Single to HA later means a new service and a data migration.
68
+
69
+ **4. Namespace core package** (`--core-package=LIGHT|SERIOUS`, step 5). `LIGHT` is free and the default on
70
+ every environment. `SERIOUS` adds $10 per namespace and a highly available balancer in front of its proxy,
71
+ and it can never be downgraded again.
72
+
29
73
  ## Secrets: rules for an agent
30
74
 
31
75
  - Every credential is read from the ENVIRONMENT ONLY. No command takes one as a flag.
32
76
  - Never print a credential, never put one in a command line you echo, never commit one.
33
- - Keep each one in its own file with mode `0600` (`umask 077` first) and pass it as
34
- `NAME="$(cat <file>)" fabrika …`. Check a file with `wc -c`, never with `cat`.
35
- - The provisioning key from step 1 is printed ONCE. Capture it from the output into its file at once.
36
- The only other copies are in the `iam` and `control` service environments.
77
+ - `platform install` writes every credential it creates to `--secrets-file`, mode `0600`, before it places
78
+ the first one. That file is the ONLY readable copy. Zerops stores them as sensitive variables and answers
79
+ `REDACTED` when they are read back, so never try to read a secret from the Zerops API, and never ask the
80
+ human for sudo mode to do it. Back the file up where the human keeps credentials.
81
+ - Load the file in a subshell, so its integration token never replaces the personal one in your shell:
82
+ `( set -a && . <secrets file> && set +a && fabrika … )`.
83
+ - Keep any other credential in its own `0600` file (`umask 077` first) and pass it as `NAME="$(cat <file>)"`.
84
+ Check a file with `wc -c`, never with `cat`.
37
85
 
38
86
  ## Step 0. Get the CLI at the release tag
39
87
 
@@ -49,60 +97,73 @@ cd fabrika-platform && bun install --frozen-lockfile
49
97
  In this guide, `fabrika` means `bun <clone>/packages/cli/src/index.ts`. Do not edit the clone: a dirty
50
98
  checkout is refused, and a local commit is not on GitHub, so namespaces cannot fetch it.
51
99
 
100
+ Install a released tag, not a branch. Every application build fetches `@fabrika/check` at the version of the
101
+ checkout, so `install` and `deploy` refuse a checkout whose version npm does not have yet: `this checkout is
102
+ version X, which is not published yet`. A `main` between a release's version bump and its publish is such a
103
+ checkout.
104
+
52
105
  ## Step 1. Install the platform
53
106
 
54
107
  ```bash
55
108
  export FABRIKA_ZEROPS_ACCESS_TOKEN="$(cat <personal token file>)"
56
109
  curl -s -H "Authorization: Bearer $FABRIKA_ZEROPS_ACCESS_TOKEN" \
57
- https://api.app-prg1.zerops.io/api/rest/public/user/info | jq '.clientUserList[] | {clientId, name: .client.accountName}'
58
- fabrika platform install --provider=zerops --create-project --client-id=<clientId> --env=<name> --source-dir=. --yes > install.log 2>&1
110
+ https://api.app-prg1.zerops.io/api/rest/public/user/info | jq '{email, clients: [.clientUserList[] | {clientId, name: .client.accountName, role: .roleCode}]}'
111
+ fabrika platform install --provider=zerops --create-project --client-id=<clientId> --env=<name> --tier=<light|standard> \
112
+ --secrets-file=<path outside the clone> --source-dir=. --yes > install.log 2>&1
59
113
  ```
60
114
 
61
- - Run it from the clone root. It runs for many minutes: it imports six services, builds the proxy once to
62
- learn the public hosts, then deploys everything.
115
+ - Check the token kind first. The `email` of a personal token is the person's address, and its `role` on the
116
+ client is `OWNER`. An `email` of `token-<id>@zerops.io` is an INTEGRATION token. The install refuses one before
117
+ it creates anything, because an integration token may not mint the token the control plane needs. Ask the
118
+ human for a personal token instead.
119
+ - Run it from the clone root. It runs for many minutes: it imports the tier's services, builds the proxy
120
+ once to learn the public hosts, then deploys everything.
63
121
  - `--yes` is required when stdin is not a terminal.
64
- - Capture the key right away and keep the log away from anything that publishes it:
65
- `grep -o 'FABRIKA_IAM_PROVISIONING_KEY=px_[A-Za-z0-9_-]*' install.log | cut -d= -f2 > provisioning.key`
122
+ - `--secrets-file` names a NEW file outside the clone, for example `~/fabrika-<env>.secrets.env`. An existing
123
+ file is refused. The log prints no credential.
66
124
  - The project id is printed BEFORE the long wait. If the run is interrupted, re-run with
67
125
  `--project-id=<id>` instead of `--create-project`.
68
126
  - A project that already holds generated secrets is refused. Do not try to force it: a second bring-up
69
127
  over the same project would destroy the vault key. Start over in a new empty project.
70
128
  - The account must be in the default region, or set `FABRIKA_ZEROPS_API_URL` for every command.
71
129
 
72
- **Check.** The log ends with `✓ iam <host>`, the console origin and the provisioning key box. Write down
73
- the three hosts: IAM, console and Operations.
130
+ **Check.** The log ends with the `Hand off` step: where the credentials are, `The console answers at
131
+ https://<console host>`, and an `ADMIT YOURSELF` box that holds the step 2 command with `--iam-host` filled
132
+ in. The secrets file exists with mode `0600`. The three hosts are printed earlier, right after pass 1, as
133
+ `✓ iam <host>`, `✓ console <host>` and `✓ operations <host>`. Write them down.
74
134
 
75
135
  ## Step 2. Admit the first administrator
76
136
 
77
137
  ```bash
78
- FABRIKA_IAM_PROVISIONING_KEY="$(cat provisioning.key)" \
79
- fabrika platform admin --provider=zerops --email=<address> --iam-host=<IAM host from step 1>
138
+ ( set -a && . <secrets file> && set +a && \
139
+ fabrika platform admin --provider=zerops --email=<address> --iam-host=<IAM host from step 1> )
80
140
  ```
81
141
 
82
- It prints one enrollment URL. The URL is a credential: give it to the human only, and do not log it. The
83
- human opens it, sets a password and signs in at the console origin. A re-run creates nothing new;
84
- `--reissue` replaces an enrollment that expired unopened.
142
+ It prints one enrollment URL and the time it expires. Nothing is e-mailed: the installation has no mail
143
+ service, so do not tell the human to wait for a message. The URL is a credential: give it to the human only,
144
+ and do not log it. The human opens it, sets a password and signs in at the console origin.
145
+
146
+ The URL is valid for ONE HOUR. Hand it over at once. A re-run creates nothing new: while the enrollment is
147
+ unused it only reports that one is outstanding, and it never prints the URL again. To get a new URL, after
148
+ expiry or after a lost one, run the same command with `--reissue`. It replaces the old enrollment, so the old
149
+ URL stops working.
85
150
 
86
151
  ## Step 3. Create the sidecar repository
87
152
 
88
153
  The sidecar is a GitHub repository the operator owns. Its workflow runs `platform deploy` at the pinned tag.
89
154
 
90
- Read the integration token that step 1 minted. Never use the personal token here:
155
+ Steps 4 and 5 do not depend on this step: the installation from step 1 already runs. The sidecar is what
156
+ upgrades it later (step 6). Run this step before or after step 5. Only `platform upgrade` needs it.
91
157
 
92
- ```bash
93
- A=https://api.app-prg1.zerops.io/api/rest/public
94
- H="Authorization: Bearer $FABRIKA_ZEROPS_ACCESS_TOKEN"
95
- control=$(curl -s -H "$H" "$A/project/<project id>/service-stack?limit=100&offset=0" | jq -r '.list[] | select(.name=="control") | .id')
96
- curl -s -H "$H" "$A/service-stack/$control/env" | jq -r '.items[] | select(.key=="FABRIKA_ZEROPS_ACCESS_TOKEN") | .content' > integration.token
97
- ```
98
-
99
- Then run `init` from a directory that is NOT inside any git repository. The fabrika clone is a repository,
100
- and a home directory under dotfiles version control is one too. Otherwise pass `--checkout=<dir>` outside one:
158
+ `init` takes the integration token step 1 minted, never the personal one. The secrets file holds it as
159
+ `FABRIKA_ZEROPS_ACCESS_TOKEN`, beside the provisioning key. Run `init` from a directory that is NOT inside
160
+ any git repository. The fabrika clone is a repository, and a home directory under dotfiles version control is
161
+ one too. Otherwise pass `--checkout=<dir>` outside one:
101
162
 
102
163
  ```bash
103
- FABRIKA_ZEROPS_ACCESS_TOKEN="$(cat integration.token)" FABRIKA_IAM_PROVISIONING_KEY="$(cat provisioning.key)" \
164
+ ( set -a && . <secrets file> && set +a && \
104
165
  fabrika platform init --provider=zerops <installation> --repo=<owner>/<name> --tag=<tag> \
105
- --project-id=<project id> --env=<name from step 1> --yes < /dev/null
166
+ --project-id=<project id> --env=<name from step 1> --yes < /dev/null )
106
167
  ```
107
168
 
108
169
  - Without `--repo` the repository is `contember/fabrika-zerops-<installation>`. Pass `--repo` for any
@@ -115,14 +176,17 @@ FABRIKA_ZEROPS_ACCESS_TOKEN="$(cat integration.token)" FABRIKA_IAM_PROVISIONING_
115
176
  ## Step 4. Mint a machine key for `fabrika control`
116
177
 
117
178
  ```bash
118
- iam=$(curl -s -H "$H" "$A/project/<project id>/service-stack?limit=100&offset=0" | jq -r '.list[] | select(.name=="iam") | .id')
119
- curl -s -H "$H" "$A/service-stack/$iam/env" | jq -r '.items[] | select(.key=="FABRIKA_IAM_RPC_KEY") | .content' > iam-rpc.key
120
- FABRIKA_IAM_RPC_URL=https://<IAM host> FABRIKA_IAM_RPC_KEY="$(cat iam-rpc.key)" FABRIKA_IAM_PROVISIONING_KEY="$(cat provisioning.key)" \
179
+ ( umask 077 && set -a && . <secrets file> && set +a && FABRIKA_IAM_RPC_URL=https://<IAM host> \
121
180
  fabrika control key issue --label=<name> --permissions=app.manage,namespace.manage,deploy.trigger,deploy.read \
122
- --expires-in=2592000 > control-key.out
123
- grep -o 'px_[A-Za-z0-9_-]*' control-key.out | head -1 > control.key && rm control-key.out
181
+ --expires-in=2592000 > control.key )
182
+ wc -c control.key && ls -l control.key
124
183
  ```
125
184
 
185
+ The secrets file supplies `FABRIKA_IAM_RPC_KEY` and `FABRIKA_IAM_PROVISIONING_KEY`.
186
+
187
+ - stdout carries the key and nothing else; the credential id goes to stderr. Keep the redirect inside the
188
+ `umask 077` subshell: `control.key` is then created with mode `0600` (`-rw-------`). Outside it, the file
189
+ is readable by other users.
126
190
  - `FABRIKA_IAM_RPC_URL` is IAM's PUBLIC `https://` origin. The private `http://iam:3000` answers only inside
127
191
  the Zerops project.
128
192
  - A key expires after at most 30 days. Mint a new one when it does.
@@ -134,30 +198,59 @@ export FABRIKA_CONTROL_URL=https://<console host>
134
198
  FABRIKA_CONTROL_KEY="$(cat control.key)" fabrika control apps list
135
199
  ```
136
200
 
137
- **Check.** `apps list` answers, with an empty list on a fresh installation.
201
+ **Check.** On a fresh installation `apps list` prints `no applications registered yet` (on stderr) and
202
+ exits 0. An authentication error means the key or the console origin is wrong.
138
203
 
139
204
  ## Step 5. Deploy the first application
140
205
 
141
- The public example is [`contember/fabrika-example-zerops`](https://github.com/contember/fabrika-example-zerops).
206
+ The example is [`contember/fabrika-example-zerops`](https://github.com/contember/fabrika-example-zerops).
142
207
  Its `fabrika.config.ts` places the app with its own database (`mid` preset), and `fabrika.cheap.config.ts`
143
208
  places it on the namespace's shared PostgreSQL (`cheap` preset). Use the config that matches the preset.
144
209
 
145
- 1. **Create a namespace.** It is the Zerops project applications are placed in. Without a custom domain
146
- bound to its proxy, pass `--public-access=zerops-subdomain`; the default is `custom-domain`, which serves
147
- no host until you bind one.
210
+ **Repository access decides how you register** (step 5.2). Check it first:
211
+ `curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/repos/<owner>/<name>`.
212
+
213
+ - `200`: the repository is public. Register it with `--installation-id=none`. Control reads it without a token.
214
+ - `404`: GitHub hides it from an anonymous caller, so it is private (or the name is wrong). `register` refuses
215
+ it with `--installation-id=none`. Ask the human to connect the GitHub App in the console first (Settings →
216
+ Source, a browser flow), then register without `--installation-id`.
217
+
218
+ **Use the installation's release in the application.** The application's `@fabrika/*` packages, the CLI
219
+ among them, must be the version of the tag you installed: for `v0.4.3`, `^0.4.3`. Another version can build
220
+ a manifest this installation's control refuses or reads differently. From the fabrika clone of step 0, run
221
+ `bun scripts/release.ts example-pin <application checkout> --tag=<tag>`. It rewrites every `@fabrika/*` range
222
+ and commits nothing. Commit and push the change in the application repository, then run `bun install` there.
223
+ Check with `bun pm ls | grep @fabrika/cli`.
224
+
225
+ 1. **Create a namespace** with the preset and core package the human chose (see Decisions). Without a
226
+ custom domain bound to its proxy, pass `--public-access=zerops-subdomain`; the default is
227
+ `custom-domain`, which serves no host until you bind one.
148
228
 
149
229
  ```bash
150
- fabrika control namespaces plan --namespace=<id> --env=<name> --preset=mid --public-access=zerops-subdomain
151
- fabrika control namespaces create --namespace=<id> --env=<name> --preset=mid --public-access=zerops-subdomain
230
+ fabrika control namespaces plan --namespace=<id> --env=<name> --preset=<cheap|mid|full> --public-access=zerops-subdomain
231
+ fabrika control namespaces create --namespace=<id> --env=<name> --preset=<cheap|mid|full> --public-access=zerops-subdomain
152
232
  fabrika control namespaces get --namespace=<id>
153
233
  ```
154
234
 
155
235
  `create` returns while the state is `pending`. Provisioning takes several minutes. Poll `get` every
156
- 30 seconds until it is `ready`, then pick a host it lists as `free`.
236
+ 30 seconds until it is `ready`.
237
+
238
+ - If the state is `failed`, `get` prints the cause and the command that resumes it on stderr. Run
239
+ `fabrika control namespaces reconcile --namespace=<id>` and poll `get` again. Provisioning resumes from
240
+ where it stopped. Do not create a second namespace.
241
+ - `get` lists one host per line as `<host> <port> free` or `<host> <port> taken by <app>/<env>`. A
242
+ `zerops-subdomain` namespace has six hosts, one per public port of its proxy: 8080 and 8082–8086. No
243
+ port is special or reserved; 8081 is the proxy's health port and is never listed. Use ANY `free` host,
244
+ for example the first one. Each host serves exactly one application environment, so six is the limit
245
+ per namespace.
157
246
 
158
247
  2. **Build and register**, from the application's repository root. The repository must be pushed: control
159
248
  deploys the commit on GitHub, not your working tree.
160
249
 
250
+ `--app=<id>` is the application id the manifest carries: `jq -r .app.id fabrika.manifest.json` after the
251
+ build. The application's `fabrika.config.ts` sets it (`defineApp({ id })`); the example takes it from its
252
+ contract's vocabulary, and it is `notes`.
253
+
161
254
  ```bash
162
255
  bun install
163
256
  bunx fabrika app build --env=<name> # cheap: add --config=fabrika.cheap.config.ts
@@ -166,8 +259,10 @@ places it on the namespace's shared PostgreSQL (`cheap` preset). Use the config
166
259
  --namespace=<namespace id> --domain=<free host> --installation-id=none
167
260
  ```
168
261
 
169
- - `--installation-id=none` is for a PUBLIC repository. A private one needs the GitHub App connected in
170
- the console (Settings → Source, a browser flow for the human); then omit the flag.
262
+ - `--installation-id=none` works only for a repository GitHub serves without a token. `register` reads it
263
+ once and refuses one that is not publicly readable, so the failure shows here and not at deploy. A
264
+ private repository needs the GitHub App connected in the console first (Settings → Source, a browser
265
+ flow for the human); then omit the flag.
171
266
  - `register` CREATES and refuses a second registration. A changed manifest goes through
172
267
  `control apps environments put` with the same placement flags.
173
268
 
@@ -179,6 +274,9 @@ places it on the namespace's shared PostgreSQL (`cheap` preset). Use the config
179
274
  FABRIKA_CONTROL_KEY="$(cat control.key)" bunx fabrika control runs log --run=<run id>
180
275
  ```
181
276
 
277
+ A failed run's log ends with `deploy failed: <reason>`. For example, `source resolve failed
278
+ (ref_not_found)` means control could not read the repository or the ref.
279
+
182
280
  The deploy fails when the repository at the deployed commit does not match the registered manifest.
183
281
  Commit and push, rebuild the manifest, `apps environments put` it, and deploy again.
184
282
 
@@ -195,12 +293,26 @@ fabrika platform upgrade --provider=zerops --to=<newer tag> --sidecar=<sidecar c
195
293
  ```
196
294
 
197
295
  It commits the tag to the sidecar's `fabrika.ref`, pushes, and follows the workflow run it triggers. It
198
- needs only the `gh` login. Existing namespaces keep their proxy version.
296
+ needs only the `gh` login. Existing namespaces keep their proxy version. To move one to the proxy of the
297
+ new release, after the run succeeded:
298
+
299
+ ```bash
300
+ fabrika control namespaces update --namespace=<id> --proxy-source=current
301
+ ```
302
+
303
+ It re-pins the namespace and queues the reconcile that rebuilds its proxy; poll `namespaces get` until it is
304
+ `ready`. A namespace pinned to a release before the proxy manifest moved into the build archive carries it in a
305
+ service variable, which Zerops caps at about 32 KB: when a deploy fails with `serviceEnvValueTooLarge`, this is
306
+ the fix, and it also accepts a namespace in `failed`.
199
307
 
200
308
  ## When something fails
201
309
 
202
310
  - Every refusal names its flag or variable. Fix that one value and re-run. `init`, `admin`, `deploy` and
203
311
  `upgrade` are safe to re-run. `install` is not: see step 1.
312
+ - A failed install pass 2 is finished with `platform deploy`, with the credentials from the secrets file. The
313
+ install prints the command.
314
+ - Lost the secrets file? Nothing in fabrika can recover it, and Zerops will not hand the values back. Start a
315
+ new installation in a new project.
204
316
  - A Zerops build or deploy failure: read the service log in the Zerops GUI or with
205
317
  `zcli service log -S <service id> --show-build-logs`.
206
318
  - Do not work around a refusal by editing the clone or the generated sidecar. Report what failed, with the
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'
13
13
  import { createZeropsApi, defaultSleep } from '../index'
14
14
  import { consoleAdminCollaborators, runPlatformAdmin } from './admin'
15
15
  import { parsePlatformAdminArgs, REISSUE_FLAG } from './admin-options'
16
+ import { npmCheckerPublication } from './checker-release'
16
17
  import { deployPlatform, PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
17
18
  import { parsePlatformDeployArgs } from './deploy-options'
18
19
  import { consoleInitCollaborators, runInit } from './init'
@@ -66,9 +67,9 @@ deploy that push triggers.
66
67
 
67
68
  ── platform install ──────────────────────────────────────────────────────────────────────────────
68
69
 
69
- The from-scratch bring-up. With \`--create-project\` it creates the project too, with core package
70
- LIGHT and \`envIsolation: service\` — the two settings Zerops accepts at project CREATION only and never
71
- afterwards. \`--project-id=<id>\` installs into an empty project you created yourself instead, with both
70
+ The from-scratch bring-up. With \`--create-project\` it creates the project too, with the tier's core
71
+ package (LIGHT or SERIOUS) and \`envIsolation: service\` — the two settings Zerops accepts at project
72
+ CREATION only and never afterwards. \`--project-id=<id>\` installs into an empty project you created yourself instead, with both
72
73
  already right; nothing here can read \`envIsolation\` back or correct it.
73
74
 
74
75
  Interactive and laptop-side. It CONFIRMS before every step that leaves your machine: reading the
@@ -79,27 +80,30 @@ is not consent.
79
80
 
80
81
  1. create the project and wait for it to be ACTIVE (\`--create-project\`; its id is printed before
81
82
  the wait), or read the one you named — and refuse a core package the tier does not match
82
- 2. import ${PLATFORM_DEPLOY_ORDER.length + 2} services without code, and wait for the import's processes
83
+ 2. import the tier's services without code, and wait for the import's processes
83
84
  3. generate six secrets and mint the control plane's Zerops INTEGRATION token
84
- (client role NO_ACCESS, ADMIN on this project only — never your personal token)
85
+ (client role NO_ACCESS, ADMIN on this project only — never your personal token), and write
86
+ all of them to \`--secrets-file\` (mode 0600) before the first one is placed
85
87
  4. pass 1: give the proxy an EMPTY manifest and build it, so it publishes one public hostname per
86
88
  HTTP port; the three platform hosts are then READ off those, never composed
87
89
  5. write every remaining variable on all four runtime services
88
90
  6. pass 2: hand the whole ordered sequence to \`platform deploy\`
89
- 7. print the provisioning key — the CLI keeps no copy; it is held by iam and control in Zerops
91
+ 7. print the next commands; they read their credentials from the secrets file. Zerops holds the
92
+ same values as SENSITIVE variables and does not hand them back, so that file is the only copy
90
93
 
91
94
  It REFUSES a project that already holds an installation's generated secrets: a second bring-up would
92
95
  roll a new vault key over the old one, which is unrecoverable, and new signing keys, which logs
93
96
  everyone out. Once every bootstrap variable is placed, \`platform deploy\` can update the installation
94
- or finish a failed pass 2. Read FABRIKA_IAM_PROVISIONING_KEY from iam or control and
95
- FABRIKA_ZEROPS_ACCESS_TOKEN from control in Zerops if the handoff was interrupted.
97
+ or finish a failed pass 2, with the credentials from the secrets file.
96
98
  Neither deploy nor init completes a partial bootstrap. If interrupted after a generated secret was
97
99
  placed but before every bootstrap variable was placed, preserve the existing keys and arrange
98
100
  operator-owned recovery, or start a fresh installation in a separate empty project.
99
101
  Re-running install after any generated secret was placed is refused.
100
102
 
101
- Only the LIGHT tier can be installed this way — it is the one topology that emits a services-only
102
- import document. Authentication is PASSWORD ONLY; no administrator exists yet when this finishes.
103
+ Two tiers, one project each. \`light\` (core LIGHT): one single-node PostgreSQL and one bucket shared by
104
+ IAM, control and Operations, one container per service. \`standard\` (core SERIOUS): an HA PostgreSQL for
105
+ IAM and control, a second HA PostgreSQL and bucket for Operations, two containers of IAM and the proxy.
106
+ Authentication is PASSWORD ONLY; no administrator exists yet when this finishes.
103
107
 
104
108
  Options (a flag beats the environment variable beside it):
105
109
 
@@ -112,7 +116,8 @@ Options (a flag beats the environment variable beside it):
112
116
  --env=<name> FABRIKA_PLATFORM_ENVIRONMENT written to every service as ENVIRONMENT
113
117
  --scheme=<http|https> FABRIKA_PLATFORM_SCHEME default https
114
118
  --source-dir=<checkout> required clean full checkout running this CLI
115
- --tier=light FABRIKA_PLATFORM_TIER default light, and the only value
119
+ --tier=<light|standard> FABRIKA_PLATFORM_TIER default light
120
+ --secrets-file=<path> FABRIKA_PLATFORM_SECRETS_FILE required: a NEW file outside the checkout
116
121
  ${UNATTENDED_FLAG} answer every confirmation yes —
117
122
  required when stdin is not a terminal
118
123
 
@@ -248,23 +253,24 @@ commit availability before its effects; a local commit is not thereby published
248
253
  The first human. A fresh installation has none — nothing seeds an admission list, and this command is
249
254
  why it does not have to. It is unattended, prompts for nothing, and changes nothing on a re-run:
250
255
 
251
- 1. find the principal holding that mailbox, or invite one
256
+ 1. find the principal holding that address, or create one
252
257
  2. grant it the CROSS-APP \`admin\` role, unless it already holds one
253
258
  3. issue ONE password enrollment and print its URL
254
259
 
255
260
  The grant is cross-app (\`app: null\`) and that is load-bearing: grants filter to the calling app, so an
256
261
  \`admin\` grant scoped to the console's own app id leaves Delivery and Operations working while the
257
- Access plane refuses. A re-run invites nobody twice, grants nothing twice and issues no second
262
+ Access plane refuses. A re-run creates nobody twice, grants nothing twice and issues no second
258
263
  enrollment — a password that is already set, or an enrollment already outstanding, is REPORTED. Pass
259
264
  \`${REISSUE_FLAG}\` to replace an outstanding one, which is the way out when the first expired unopened.
260
265
 
261
- The enrollment URL is a credential: it is printed once, on a line of its own, and stored nowhere.
266
+ Nothing is e-mailed: a Zerops installation has no mail service. Hand the URL to the administrator
267
+ yourself. It is a credential: it is printed once, on a line of its own, and stored nowhere.
262
268
 
263
269
  \`install\` reports the host this needs as \`✓ iam <host>\` and ends with this command ready to run.
264
270
 
265
271
  Options (a flag beats the environment variable beside it):
266
272
 
267
- --email=<address> FABRIKA_PLATFORM_ADMIN_EMAIL the administrator's mailbox
273
+ --email=<address> FABRIKA_PLATFORM_ADMIN_EMAIL the administrator's sign-in address
268
274
  --iam-host=<host> FABRIKA_PLATFORM_IAM_HOST IAM's public hostname, as \`deploy\` names it
269
275
  --scheme=<http|https> FABRIKA_PLATFORM_SCHEME default https
270
276
  ${REISSUE_FLAG} replace an outstanding enrollment
@@ -347,6 +353,7 @@ const runDeploy = async (argv: readonly string[]): Promise<void> => {
347
353
  const input = parsePlatformDeployArgs(argv, process.env)
348
354
  await deployPlatform(input, {
349
355
  prepareSource: prepareInstallationSource,
356
+ checkerPublished: npmCheckerPublication(),
350
357
  uploadFetch: (url, init) => fetch(url, init),
351
358
  api: createZeropsApi({
352
359
  token: input.accessToken,
@@ -138,9 +138,9 @@ export const parseInitArgs = (argv: readonly string[], env: CommandEnv, cwd: str
138
138
  accessToken: credential(
139
139
  env,
140
140
  'FABRIKA_ZEROPS_ACCESS_TOKEN',
141
- "the Zerops INTEGRATION token `platform install` minted, read from the `control` service's environment",
141
+ 'the Zerops INTEGRATION token `platform install` minted, from its secrets file',
142
142
  ),
143
- provisioningKey: credential(env, 'FABRIKA_IAM_PROVISIONING_KEY', 'the px_ key `platform install` printed'),
143
+ provisioningKey: credential(env, 'FABRIKA_IAM_PROVISIONING_KEY', 'the px_ key from the secrets file `platform install` wrote'),
144
144
  unattended: argv.includes(UNATTENDED_FLAG),
145
145
  ...(hosts === undefined ? {} : { hosts }),
146
146
  ...(apiBaseUrl === undefined ? {} : { apiBaseUrl }),
@@ -95,8 +95,14 @@ export const inspectPlatform = async (projectId: string, api: ZeropsApi, signal:
95
95
  if (control === undefined || control.id === '') throw new Error(`Zerops project ${projectId} has no \`control\` service`)
96
96
  const proxy = oneService(services, 'proxy')
97
97
  if (proxy === undefined || proxy.id === '') throw new Error(`Zerops project ${projectId} has no \`proxy\` service`)
98
- const readEnv = async (serviceId: string): Promise<Map<string, string>> =>
99
- new Map((await api.listServiceEnv({ serviceId, signal })).map((item) => [item.key, item.content]))
98
+ // Only the readable values: everything init prints is derived from non-secret configuration.
99
+ const readEnv = async (serviceId: string): Promise<Map<string, string>> => {
100
+ const readable = new Map<string, string>()
101
+ for (const item of await api.listServiceEnv({ serviceId, signal })) {
102
+ if (!item.sensitive) readable.set(item.key, item.content)
103
+ }
104
+ return readable
105
+ }
100
106
  return {
101
107
  controlEnv: await readEnv(control.id),
102
108
  proxyEnv: await readEnv(proxy.id),
@@ -1,6 +1,7 @@
1
1
  import type { Confirmations } from '@fabrika/installation-init'
2
2
  import type { SchemaReconciler } from '@fabrika/provider-contract'
3
3
  import type { Sleeper, ZeropsApi } from '../index'
4
+ import type { CheckerPublication } from './checker-release'
4
5
  import type { PlatformSourcePreparer } from './installation-source'
5
6
  import type { InitLog } from './log'
6
7
  import type { PlatformUploadFetch } from './platform-upload'
@@ -11,6 +12,7 @@ export type InstallPrompts = Confirmations
11
12
  /** Everything `runInstall` reaches the outside world through. */
12
13
  export interface InstallCollaborators {
13
14
  readonly prepareSource: PlatformSourcePreparer
15
+ readonly checkerPublished: CheckerPublication
14
16
  readonly uploadFetch: PlatformUploadFetch
15
17
  readonly api: ZeropsApi
16
18
  readonly reconcileSchema: SchemaReconciler
@@ -18,6 +20,8 @@ export interface InstallCollaborators {
18
20
  readonly log: InitLog
19
21
  readonly prompts: InstallPrompts
20
22
  readonly signal: AbortSignal
23
+ /** Create the local secrets file (`--secrets-file`) with this text. Never logged. */
24
+ readonly storeSecrets: (text: string) => Promise<void>
21
25
  }
22
26
 
23
27
  export const assertRunning = (signal: AbortSignal): void => {