@astrale-domains/services 0.4.7 → 0.4.8
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 +438 -43
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ protocols behind neutral SDK Integrations and stores only admitted lifecycle evi
|
|
|
30
30
|
same Mutation: the Kernel deletes a Node only together with every incident Edge. Its lines stay
|
|
31
31
|
registered with no holder and are refused to every organisation. A transfer is a root
|
|
32
32
|
`createEdge` of `line_owned_by` from the line to the new organisation; a line has one holder at
|
|
33
|
-
most.
|
|
33
|
+
most ([Transfer a line](#transfer-a-line)).
|
|
34
34
|
- secret values stay write-only at Cloudflare and never enter Astrale mutations or graph state.
|
|
35
35
|
- schedules remain provider state; only the owner may read or replace them.
|
|
36
36
|
- logs are remote evidence admitted by the Cloudflare Provider before a Workflow returns them.
|
|
@@ -123,7 +123,7 @@ the routing KV are shared. Any other Kernel finds hosting unavailable before any
|
|
|
123
123
|
Only a runtime whose Environment vars set `SERVICES_DEPLOYMENT_HOST` (the deployments' routing
|
|
124
124
|
domain) hosts deployments; every other runtime answers `hosting-unavailable`. Such a runtime refuses
|
|
125
125
|
to boot without `SERVICES_DEPLOYMENT_KERNEL` (the Admin Kernel's issuer URL),
|
|
126
|
-
`SERVICES_SECRET_DIGEST_KEY` (at least 32
|
|
126
|
+
`SERVICES_SECRET_DIGEST_KEY` (at least 32 UTF-8 bytes, kept for the life of the data plane: the
|
|
127
127
|
recorded digests depend on it), the `SERVICE_DEPLOYMENTS` dispatch binding to the environment's
|
|
128
128
|
deployments namespace, and `CLOUDFLARE_SCHEDULE_KV_ID`, the routing KV that activates a label, and
|
|
129
129
|
refuses a `SERVICES_PLATFORM_OWNER` that is not an Identity id. Hosting is declared once, by the
|
|
@@ -189,7 +189,8 @@ release digest at the same URL. A `withdrawn` tombstone is never removed.
|
|
|
189
189
|
`SERVICES_REGISTRY_ORIGIN` and `SERVICES_PREVIEW_EXPIRY` (`plan`, the default, or `enforce`) are
|
|
190
190
|
Environment vars of a hosting runtime, refused in a secrets file and refused without
|
|
191
191
|
`SERVICES_DEPLOYMENT_HOST`. Without a registry origin no caller is the registry, and `retain` and
|
|
192
|
-
`expirePreviews` are refused. `admin-beta`
|
|
192
|
+
`expirePreviews` are refused. `admin` and the frozen `admin-beta` name `admin.astrale.ai` and keep
|
|
193
|
+
expiry a plan.
|
|
193
194
|
Reading last calls needs the API token's Account Analytics Read permission. `pnpm
|
|
194
195
|
provider:deployment-audit` lists the deployments namespace read-only
|
|
195
196
|
([operator](providers/cloudflare/operator/README.md)).
|
|
@@ -206,6 +207,246 @@ applies `OwnService` to each one. Workflow addresses are `CloudflareWorker.*`; a
|
|
|
206
207
|
keys are not executable. A future provider Class must supply its own concrete declarations and
|
|
207
208
|
implementations. Properties and ownership relationships retain their inherited meaning.
|
|
208
209
|
|
|
210
|
+
## Operating hosted deployments
|
|
211
|
+
|
|
212
|
+
### Recorded choices
|
|
213
|
+
|
|
214
|
+
The deployment model left four choices open (decisions D12, D13 and D16 of the deployment and
|
|
215
|
+
versioning spec, and the rule for deployment secrets). Services implements them as follows.
|
|
216
|
+
|
|
217
|
+
| Choice | Implemented here |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| How a line reads in a deployment URL | Only the platform organisation (`SERVICES_PLATFORM_OWNER`) registers or deploys readable lines, which read `<readable>-<fingerprint>`, such as `agencies-staging-svkrguuhpcvmxxas`. Every other line is its 16-character fingerprint alone, derived from the origin and environment, never a stored UUID. A line keeps the addressing it was registered with, whoever holds it later: a deploy with the other addressing is refused `address-mismatch`, and a readable deploy for any other organisation `invalid-deployment`. |
|
|
220
|
+
| Which deployment runs a Domain's crons | None. A hosted deployment's frozen configuration has no trigger, a Workers for Platforms script cannot own one, and the dispatcher's minute fan-out reads only the `cron:` keys of Services, which no deployment has. The spec's recommendation, crons on the deployment of the version the author designates (by default the highest stable one), needs the registry and is not implemented by the foundation. |
|
|
221
|
+
| Where deployments live | In the Admin instance's Services, on a line held by the publisher's organisation, outside every instance that installs them. Uninstalling a Domain, deleting an instance or deleting its Services never touches them. The organisation is an Identity of the Admin instance that the deploy request names; a request that names none deploys for the caller itself. |
|
|
222
|
+
| Deployment secrets | A deploy binds the environment's current secrets to the new deployment only; every other deployment keeps its own. A secret rotates in place on one deployment, which keeps its script, URL, issuer and key: by `setSecret`, or by deploying the identical release again. The signing key never rotates. |
|
|
223
|
+
|
|
224
|
+
### Rotate a deployment's secrets
|
|
225
|
+
|
|
226
|
+
Rotating a secret never creates a deployment. Two calls write a new value in place on one
|
|
227
|
+
deployment, and on no other:
|
|
228
|
+
|
|
229
|
+
- deploying the identical release again (same build and configuration) with the new value in the
|
|
230
|
+
environment's secrets: it is the same label, `deploy` answers `reused`, and its `secrets.state` is
|
|
231
|
+
`updated` when a value changed;
|
|
232
|
+
- `setSecret` on the deployment, for one name its frozen configuration declares.
|
|
233
|
+
|
|
234
|
+
A deployment is called by the `id` that `list` returns. Every call below runs on the Admin instance
|
|
235
|
+
(`--admin`, or `--admin=<bookmark>`), as an identity that acts for the organisation holding the line
|
|
236
|
+
(`--as <identity>` when it is not the bookmark's default), with an `astrale` CLI whose `call` takes
|
|
237
|
+
`--admin` and `-d @file`. Put `key=value` params before `--admin`.
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
astrale call /:services.astrale.ai:class.CloudflareDeployment:list \
|
|
241
|
+
origin=agencies.example.com environment=staging --admin --json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`list` answers `{ "deployments": [...] }` with each deployment's `id`, `label`, `url`, `state` and
|
|
245
|
+
declared `record`.
|
|
246
|
+
|
|
247
|
+
Never pass a secret value in argv, where `ps` and shell history keep it. Write the input to a file
|
|
248
|
+
that only you can read (`umask 077`, then your editor), outside the repository:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{ "name": "API_TOKEN", "value": "<the new value>" }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
|
|
256
|
+
--admin -d @rotate.json --json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
or pipe it from your secret manager on stdin (`read-new-secret` stands for the command that prints
|
|
260
|
+
only the new value):
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
read-new-secret | jq -Rs '{ name: "API_TOKEN", value: rtrimstr("\n") }' \
|
|
264
|
+
| astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
|
|
265
|
+
--admin -d - --json
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`secrets` lists the names a deployment binds, never a value:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.secrets" --admin --json
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
A refusal is the call's value (`"kind": "rejected"`); the two errors fail the call.
|
|
275
|
+
|
|
276
|
+
| Reply | Meaning | Then |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `{ "name": "API_TOKEN" }` | The value is written, or was already the one served. Propagation is eventually consistent: the provider serves a new value about 4 s later. | Nothing. Calls in those seconds may still see the old value. |
|
|
279
|
+
| `secret-undeclared` | The deployment's frozen configuration binds no such name. Nothing was written. | A new name is a new release: declare it and deploy. |
|
|
280
|
+
| `service-unavailable` with `retryAfter` | The deployment is being created, another operation holds it, or the provider failed. | The same call again after `retryAfter`. A rerun converges, whatever an interrupted one wrote. |
|
|
281
|
+
| `service-unavailable` without `retryAfter` | The deployment cannot be rotated in place: its creation failed, it is retiring or retired, or the script under its label is not the recorded one. `message` says which. | Deploy the release again after a failed creation. A retired deployment serves nothing. Ask the Services operator about a script that is not the recorded one. |
|
|
282
|
+
| `DEPLOYMENT_NOT_FOUND` | The caller neither holds the deployment's line nor acts for its holder. Nothing was read. | Call as an identity of the organisation holding the line. |
|
|
283
|
+
| `DEPLOYMENT_HOSTING_UNAVAILABLE` | The call reached a Kernel other than the Admin instance's, or the Services runtime there hosts no deployments (its Environment sets no `SERVICES_DEPLOYMENT_HOST`). | Call with `--admin`. If the Admin instance still answers it, ask the Services operator. |
|
|
284
|
+
|
|
285
|
+
### A leaked secret
|
|
286
|
+
|
|
287
|
+
A deploy never rewrites another deployment's secrets, so a leaked value stays bound to every
|
|
288
|
+
deployment that still serves it until each one is rotated:
|
|
289
|
+
|
|
290
|
+
1. Put the new value everywhere a deploy reads the environment's secrets from: the secrets file the
|
|
291
|
+
environment's adapter names (its `secrets` option, such as `.env.staging`) and every CI secret
|
|
292
|
+
store that deploys the environment. From then on, until step 5, no deploy may run with the old
|
|
293
|
+
value: an identical redeploy with it would write it back in place (`secrets.state: updated`).
|
|
294
|
+
2. Rotate every deployment of that origin and environment that serves, with the input file above.
|
|
295
|
+
`list` shows a deployment only once it has served: a deployment still being created, or whose
|
|
296
|
+
creation failed, is not listed. The block reads the list on each run, so run it again until
|
|
297
|
+
every reply is `{ "name": ... }`. A deployment already rotated answers the same without writing
|
|
298
|
+
anything, and one that answered `service-unavailable` with `retryAfter` takes the rotation on a
|
|
299
|
+
later run.
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
for ID in $(astrale call /:services.astrale.ai:class.CloudflareDeployment:list \
|
|
303
|
+
origin=agencies.example.com environment=staging --admin --json \
|
|
304
|
+
| jq -r '.deployments[] | select(.state == "active") | .id'); do
|
|
305
|
+
astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
|
|
306
|
+
--admin -d @rotate.json --json
|
|
307
|
+
done
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
An identical redeploy rotates only the deployment of the current release; older ones need
|
|
311
|
+
`setSecret`. Only `active` deployments rotate in place: a retiring or retired one answers
|
|
312
|
+
`service-unavailable` without `retryAfter`, so revoking the value at its source (step 5) is what
|
|
313
|
+
covers it. A deployment still being created, or whose creation failed, is step 3's.
|
|
314
|
+
3. Take over every creation that a deploy started before step 1: that deploy may have read the old
|
|
315
|
+
value. Its deployment stays `deploying`, or `failed`, until a deploy records it active, so `list`
|
|
316
|
+
does not show it and `setSecret` refuses it; yet it serves as soon as its deploy has published
|
|
317
|
+
its record. Lease expiry alone does not stop that deploy. Activation checks only that the
|
|
318
|
+
deployment still holds the lease the deploy took, so a deploy that stalls past its lease, which
|
|
319
|
+
ends 10 minutes after it started, can still activate the deployment with the old value until
|
|
320
|
+
another deploy takes it over.
|
|
321
|
+
|
|
322
|
+
Find these deployments on the Admin instance (`ADMIN_INSTANCE` is its instance bookmark, as in
|
|
323
|
+
[Transfer a line](#transfer-a-line)). The query answers the deployments of the lines you hold or
|
|
324
|
+
act for, those still being created included. Keep the `label` of each one whose `state` is
|
|
325
|
+
`deploying` or `failed` and whose `record` names this origin and environment. When the answer
|
|
326
|
+
has a `page.next`, the same query with `--cursor <page.next>` reads the next page.
|
|
327
|
+
|
|
328
|
+
```sh
|
|
329
|
+
astrale query --class /:services.astrale.ai:class.CloudflareDeployment --limit 100 --json \
|
|
330
|
+
-i "$ADMIN_INSTANCE"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
For each label, deploy its release again, identical (the same build and configuration; with
|
|
334
|
+
adapter-astrale, `astrale-domain deploy <environment>` from the same source), now that the
|
|
335
|
+
secrets file holds the new value. Repeat until it answers `deployed` for that label:
|
|
336
|
+
- while the stalled deploy's lease runs, it answers `service-unavailable` with `retryAfter`, the
|
|
337
|
+
end of that lease: deploy again then;
|
|
338
|
+
- once the lease has expired, it takes the deployment over, so the stalled deploy can no longer
|
|
339
|
+
activate it, and creates it with the current secrets, rewriting them on any script the stalled
|
|
340
|
+
deploy left;
|
|
341
|
+
- after a failed creation it retries with the current secrets; once the stalled deploy has
|
|
342
|
+
activated the deployment, it reuses it and rewrites its secrets in place
|
|
343
|
+
(`secrets.state: updated`).
|
|
344
|
+
|
|
345
|
+
A release that does not rebuild identically has another label and takes nothing over: the
|
|
346
|
+
stalled deployment keeps the value its deploy wrote until step 5 revokes it. Then run step 2's
|
|
347
|
+
block once more.
|
|
348
|
+
4. Deployments in the platform dispatch namespace have no Services node, and neither `list` nor the
|
|
349
|
+
query shows them. They are the platform Domains deployed through adapter-cloudflare's namespace
|
|
350
|
+
mode: Shell, the Services runtime, Issues, ai-gateway, closing, grc, integrations and ui, each
|
|
351
|
+
once it has moved there. Rotate each one in place with the platform namespace's tooling, with
|
|
352
|
+
its operator file sourced
|
|
353
|
+
([operator README](./providers/cloudflare/operator/README.md#operating-platform-deployments)): one
|
|
354
|
+
label and one declared name per run, the value read on stdin from a file that holds only the new
|
|
355
|
+
value, outside the repository and readable only by you.
|
|
356
|
+
|
|
357
|
+
```sh
|
|
358
|
+
pnpm provider:platform-secret -- --label <label> --name API_TOKEN < new-value
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Or deploy its identical release again. A platform deploy that started before step 1 may have
|
|
362
|
+
read the old value too: once it has returned, rotate the deployment it made or reused. Admin is
|
|
363
|
+
not one of them: it runs on its own bootstrap Worker, and its secrets rotate through that
|
|
364
|
+
Worker's own deploy tooling. The Services runtime is one of them once the Admin instance runs on
|
|
365
|
+
a deployment of [`admin`](#admin-hosted-runtime); its identical redeploy is
|
|
366
|
+
`pnpm run deploy admin`, with the new value in `.env.admin`. Until that move it is the frozen
|
|
367
|
+
`admin-beta` Worker: put the new value in `.env.admin-beta` (and the same in `.env.admin`, which
|
|
368
|
+
keeps its values) and deploy it again with `pnpm run deploy admin-beta`, from the commit it was
|
|
369
|
+
last deployed from so that only its secrets change.
|
|
370
|
+
5. Revoke the leaked value where it was issued once step 2's block answered `{ "name": ... }` for
|
|
371
|
+
every deployment and every redeploy of step 3 answered `deployed`, or at once when the leak
|
|
372
|
+
outweighs the failures of deployments not rotated yet.
|
|
373
|
+
6. Delete `rotate.json` and `new-value`.
|
|
374
|
+
|
|
375
|
+
### Transfer a line
|
|
376
|
+
|
|
377
|
+
A line belongs to the first organisation that deploys on it. No Services Method moves it: an
|
|
378
|
+
operator transfers it as the Admin instance's root, with one Mutation on its `line_owned_by` Edge.
|
|
379
|
+
Its deployments stay on the line and keep serving. From that Mutation on, the new holder lists,
|
|
380
|
+
rotates and deploys them, and the previous holder and its members no longer see them.
|
|
381
|
+
|
|
382
|
+
1. Find the line and its holder on the Admin instance (`ADMIN_INSTANCE` is its bookmark,
|
|
383
|
+
`ADMIN_ROOT` its root identity, `LINE` the line's id):
|
|
384
|
+
|
|
385
|
+
```sh
|
|
386
|
+
astrale query --class /:services.astrale.ai:class.DeploymentLine --limit 100 --json \
|
|
387
|
+
-i "$ADMIN_INSTANCE" --as "$ADMIN_ROOT"
|
|
388
|
+
astrale query "@$LINE" --edge /:services.astrale.ai:class.line_owned_by \
|
|
389
|
+
--direction outgoing --json -i "$ADMIN_INSTANCE" --as "$ADMIN_ROOT"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Pick the line whose `origin` and `environment` are the ones to transfer. The first query reads
|
|
393
|
+
one page: when its answer has a `page.next`, more lines exist, and the same query with
|
|
394
|
+
`--cursor <page.next>` reads the next page.
|
|
395
|
+
|
|
396
|
+
2. Write the Mutation. A held line moves in one atomic Mutation that deletes the current holder's
|
|
397
|
+
Edge and creates the new one; if the current holder is not the one named, nothing moves:
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"preconditions": [],
|
|
402
|
+
"operations": [
|
|
403
|
+
{
|
|
404
|
+
"op": "edge.delete",
|
|
405
|
+
"edge": {
|
|
406
|
+
"class": "services.astrale.ai:class.line_owned_by",
|
|
407
|
+
"source": "@<line-id>",
|
|
408
|
+
"target": "@<current-holder-id>"
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"op": "edge.create",
|
|
413
|
+
"class": "services.astrale.ai:class.line_owned_by",
|
|
414
|
+
"source": "@<line-id>",
|
|
415
|
+
"target": "@<new-organization-id>",
|
|
416
|
+
"props": {}
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
A line without a holder, after its organisation was deleted, takes the `edge.create` operation
|
|
423
|
+
alone. The Kernel refuses a second holder, so that form never moves a held line.
|
|
424
|
+
|
|
425
|
+
3. Check the document offline, then apply it:
|
|
426
|
+
|
|
427
|
+
```sh
|
|
428
|
+
astrale mutate --file transfer.json --dry
|
|
429
|
+
astrale mutate --file transfer.json -i "$ADMIN_INSTANCE" --as "$ADMIN_ROOT"
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
A readable line stays readable: give it only to the platform organisation, since any other one is
|
|
433
|
+
refused on it. Changing `SERVICES_PLATFORM_OWNER` therefore means transferring every readable line
|
|
434
|
+
to the new platform organisation. Deleting an organisation that holds lines leaves them without a
|
|
435
|
+
holder (see [Model](#model)): their deployments keep serving, and nobody can list, rotate or deploy
|
|
436
|
+
them until they are transferred.
|
|
437
|
+
|
|
438
|
+
### Orphans
|
|
439
|
+
|
|
440
|
+
A hosted deployment belongs to its line, never to the instances that install it: deleting or
|
|
441
|
+
retiring an instance leaves it alone, and the orphan audit and reclaim of the
|
|
442
|
+
[operator README](./providers/cloudflare/operator/README.md) never select it. What can be left over:
|
|
443
|
+
|
|
444
|
+
| Situation | What holds | Then |
|
|
445
|
+
| --- | --- | --- |
|
|
446
|
+
| A deploy interrupted before activation | The graph keeps the deployment `deploying` under its lease, or `failed`, and `list` does not show it. An uploaded script serves nothing until its deploy publishes its record; one interrupted after that serves while the graph still says `deploying` or `failed`. Until another deploy takes it over, the interrupted deploy can still activate it, even after its lease expired. | Deploy the same release again. It resumes on the same label, takes over an expired lease, and adopts the uploaded script only if its tags name the same artifact, line, key and Admin Kernel, rewriting its secrets with the environment's current ones. |
|
|
447
|
+
| A line whose organisation was deleted | No organisation holds it; every deploy on it is refused `line-owned-elsewhere`. Its deployments keep serving, and nobody can rotate them. | Transfer the line. |
|
|
448
|
+
| A script under a label that is not the recorded one | `deploy` answers `deployment-conflict` and `setSecret` `service-unavailable` without `retryAfter`; nothing is written to it. | The operator compares its `astrale-artifact:`, `astrale-line:`, `astrale-key:` and `astrale-install:` tags with the deployment's record and key. |
|
|
449
|
+
|
|
209
450
|
## Shared public ingress
|
|
210
451
|
|
|
211
452
|
New Services use the environment's proxied wildcard DNS, wildcard Worker route and wildcard
|
|
@@ -257,15 +498,15 @@ data.
|
|
|
257
498
|
|
|
258
499
|
## Admin-hosted runtime
|
|
259
500
|
|
|
260
|
-
The
|
|
261
|
-
publishers' deployments outside the instances that install them.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
(`ASTRALE_ROUTER_WORKER`), the routing domain under which deployments are addressed
|
|
501
|
+
The Admin-hosted runtime is the Services runtime of the Admin instance, the one that hosts
|
|
502
|
+
publishers' deployments outside the instances that install them. Only the Admin instance installs
|
|
503
|
+
it; consumer instances keep the Services they pin. It runs on the beta data plane the Admin
|
|
504
|
+
instance's Services already manage. Its plain vars name the router Worker
|
|
505
|
+
(`ASTRALE_ROUTER_WORKER`), the routing domain under which hosted deployments are addressed
|
|
265
506
|
(`SERVICES_DEPLOYMENT_HOST`, served by the Services dispatcher), the Admin instance's Kernel, the
|
|
266
507
|
only one it hosts deployments for (`SERVICES_DEPLOYMENT_KERNEL`, `https://admin.eu.beta.astrale.ai/api`),
|
|
267
508
|
and the registry Domain (`SERVICES_REGISTRY_ORIGIN`). These vars are the only declaration of
|
|
268
|
-
hosting: they also bind `SERVICE_DEPLOYMENTS`.
|
|
509
|
+
hosting: they also bind `SERVICE_DEPLOYMENTS`. Its secrets file must set
|
|
269
510
|
`SERVICES_SECRET_DIGEST_KEY`, the key of each hosted deployment's secret digest, and the dispatch
|
|
270
511
|
namespace and routing KV the runtime boots with. The runtime keys HMAC-SHA256 with the UTF-8 bytes
|
|
271
512
|
of the digest key as written, never base64-decoded, so the configuration refuses, before any
|
|
@@ -274,38 +515,56 @@ sets a hosting var. `SERVICES_PLATFORM_OWNER` is not set yet: until it names the
|
|
|
274
515
|
organisation's Identity id on the Admin instance, every line is opaque and a readable deploy is
|
|
275
516
|
refused.
|
|
276
517
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
518
|
+
Two Environments deploy it:
|
|
519
|
+
|
|
520
|
+
- **`admin`**, the canonical one. Every `pnpm run deploy admin` makes one immutable deployment of
|
|
521
|
+
the release in the platform dispatch namespace (`astrale-platform`), on the readable line
|
|
522
|
+
`services-admin-6ze5tudscrzgbrbv`, at `https://<label>.platform.astrale.ai`. The deployment is
|
|
523
|
+
its own issuer, signs with a key generated for it, and binds its candidate namespace
|
|
524
|
+
(`SERVICE_CANDIDATES`) and the deployments namespace (`SERVICE_DEPLOYMENTS`) at its create-only
|
|
525
|
+
upload, both derived from `CLOUDFLARE_DISPATCH_NAMESPACE`. Nothing is ever replaced in place: a
|
|
526
|
+
new release is a new URL, which the Admin instance moves to by a consented install. Redeploying
|
|
527
|
+
the same release reuses its deployment and rewrites only that deployment's secrets in place. Its
|
|
528
|
+
secrets file is `.env.admin`.
|
|
529
|
+
- **`admin-beta`**, frozen (`providers/cloudflare/operator/provision/legacy/admin-beta.ts`). It is
|
|
530
|
+
a Worker of its own, `services-domain-admin-beta` at `https://services-admin.beta.astrale.ai`,
|
|
531
|
+
that retains the issuer `https://services.beta.astrale.ai` and its signing key, as services-v2
|
|
532
|
+
does, so the Admin instance installs it without an issuer change (OPS-3). The cost: a Worker built
|
|
533
|
+
from main holds the key that 1Pact's installations trust. Keep the key only in managed secret
|
|
534
|
+
storage and in this Worker's `ASTRALE_SIGNING_IDENTITY` secret. It changes only for a same-Schema
|
|
535
|
+
fix the Admin instance needs before it moves; everything else goes to `admin`. It is deleted from
|
|
536
|
+
this repository by D19, once the Admin instance runs on `admin` and OPS-6 retired the Worker.
|
|
537
|
+
|
|
538
|
+
### Set up the frozen admin-beta runtime (OPS-3)
|
|
539
|
+
|
|
540
|
+
The operator sets it up once, from a commit of `main` that carries the Services CT16 adoption
|
|
541
|
+
(`bindings.dispatchNamespaces` and `settings.durableOptions` in the deployment configuration, with
|
|
542
|
+
the tenant refusal) and pins an SDK release that carries the CT16 extension (R6a' or later). An
|
|
543
|
+
admin-beta built without that adoption refuses every deploy of an adapter-astrale that carries it
|
|
544
|
+
with `1003 FUNCTION_INPUT_INVALID`, and the move to `admin` over the deployments such a runtime wrote
|
|
545
|
+
is refused with `4001 DATA_MIGRATION_REQUIRED` (see [OPS-5](#move-the-admin-instance-to-admin-ops-5)).
|
|
288
546
|
|
|
289
547
|
1. Read what the Admin instance's Services are bound to, read-only:
|
|
290
548
|
`astrale introspect services.astrale.ai -i <admin-instance> --json`. Record `revision` and
|
|
291
549
|
`publication.identity.issuer`, and the frozen runtime whose `/.well-known/astrale/domain.json`
|
|
292
550
|
declares that revision: it is the rollback URL. Go on only if the issuer is
|
|
293
|
-
`https://services.beta.astrale.ai`; any other issuer makes step 6 an issuer change, which
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
551
|
+
`https://services.beta.astrale.ai`; any other issuer makes step 6 an issuer change, which this
|
|
552
|
+
runtime does not take (use the [move to `admin`](#move-the-admin-instance-to-admin-ops-5)
|
|
553
|
+
instead). Check as well that the Admin instance's Kernel issuer is the
|
|
554
|
+
`SERVICES_DEPLOYMENT_KERNEL` var; otherwise every deploy answers `hosting-unavailable`. Before
|
|
555
|
+
the first readable deploy, read the platform organisation's Identity id there and set it as the
|
|
556
|
+
`SERVICES_PLATFORM_OWNER` var in a reviewed change.
|
|
298
557
|
2. Create `.env.admin-beta` (mode 600) from `.env.example` with the beta data-plane coordinates the
|
|
299
558
|
Admin instance's Services manage today (account, token, dispatch namespace, routing domain
|
|
300
559
|
`svc.eu.beta.astrale.ai`, schedule KV, dispatcher, zone, Admin target) and a new
|
|
301
|
-
`SERVICES_SECRET_DIGEST_KEY` that no other
|
|
302
|
-
random bytes, kept as their 44-character base64 text
|
|
560
|
+
`SERVICES_SECRET_DIGEST_KEY` that no other data plane uses, e.g. `openssl rand -base64 32` (32
|
|
561
|
+
random bytes, kept as their 44-character base64 text; the configuration refuses fewer than 32
|
|
562
|
+
UTF-8 bytes). It is generated once here, copied unchanged into `.env.admin` at OPS-5, and never
|
|
563
|
+
regenerated: the recorded secret digests of every hosted deployment depend on it.
|
|
303
564
|
3. Restore the retained `https://services.beta.astrale.ai` signing identity, the one services-v2 was
|
|
304
565
|
deployed with, from managed secret storage to `.astrale/services-beta-identity.json` (mode 600).
|
|
305
566
|
Never generate a key for this runtime: the frozen R1 Worker publishes the issuer's keys, so a new
|
|
306
|
-
key would never verify.
|
|
307
|
-
Environment's default identity: `pnpm dev` would bind it to development Workers. Check that the
|
|
308
|
-
issuer publishes the restored key:
|
|
567
|
+
key would never verify. Check that the issuer publishes the restored key:
|
|
309
568
|
|
|
310
569
|
```sh
|
|
311
570
|
node --input-type=module -e '
|
|
@@ -329,14 +588,149 @@ The operator sets it up once:
|
|
|
329
588
|
The install must succeed and the Service must stay manageable (list, secrets, logs). Then install
|
|
330
589
|
the step 1 rollback URL there again: if the Kernel refuses it, step 6 is one-way, and that must be
|
|
331
590
|
recorded before step 6 runs.
|
|
332
|
-
6. Install on the Admin instance only
|
|
333
|
-
`astrale domain install https://services-admin.beta.astrale.ai --
|
|
591
|
+
6. Install on the Admin instance only:
|
|
592
|
+
`astrale domain install https://services-admin.beta.astrale.ai --allow-identity-override -i <admin-instance>`.
|
|
334
593
|
The only consent is the identity override, because the origin `services.astrale.ai` differs from
|
|
335
594
|
the serving host; the issuer does not change. On any refusal, stop: never uninstall Services on
|
|
336
595
|
the Admin instance to get around it, which would drop the Service records it holds. Never repoint
|
|
337
596
|
Admin's catalog entry for `services.astrale.ai` at this runtime: consumer flows, 1Pact's among
|
|
338
597
|
them, install Services by origin through that entry.
|
|
339
598
|
|
|
599
|
+
### Move the Admin instance to admin (OPS-5)
|
|
600
|
+
|
|
601
|
+
OPS-5 is the production operation that moves the Admin instance's Services off the frozen
|
|
602
|
+
`admin-beta` Worker onto a deployment of `admin`. Before acting, write a compatibility record and
|
|
603
|
+
know the rollback path. Afterwards, verify the Admin instance and add an entry to the operations
|
|
604
|
+
log. 1Pact's instances and every consumer instance are out of scope: they keep the Services they
|
|
605
|
+
pin, and nothing here touches them.
|
|
606
|
+
|
|
607
|
+
Preconditions:
|
|
608
|
+
|
|
609
|
+
1. The Admin instance installs `admin-beta` (OPS-3), built with the Services CT16 adoption; its
|
|
610
|
+
rollback URL is `https://services-admin.beta.astrale.ai`.
|
|
611
|
+
2. The Admin instance's Host runs a Kernel that lists installed releases and takes issuer consent
|
|
612
|
+
(astrale-os/kernel#1124, in a released Host image, OPS-4). An older Kernel refuses
|
|
613
|
+
`--allow-issuer-change` before any install, so the move waits.
|
|
614
|
+
3. The operator's `astrale` CLI is a release with install by URL (astrale-os/cli#582) and issuer
|
|
615
|
+
consent (astrale-os/cli#583).
|
|
616
|
+
4. This package pins an SDK release whose adapter-cloudflare deploys namespace mode with dispatch
|
|
617
|
+
namespace bindings and durable options (astrale-os/sdk#599, on the CT16 extension of
|
|
618
|
+
astrale-os/sdk#587), and the publishers' adapter-astrale passes `dispatchNamespaces: []`
|
|
619
|
+
(astrale-os/sdk#598).
|
|
620
|
+
5. The platform namespace is provisioned (`pnpm provider:platform-bootstrap`, OPS-1) and the deploy
|
|
621
|
+
token may manage scripts in it.
|
|
622
|
+
|
|
623
|
+
Steps:
|
|
624
|
+
|
|
625
|
+
0. **Schema.** A move, and a rollback, is a Schema upgrade: the Kernel checks the data the Admin
|
|
626
|
+
instance stores against the Schema of the URL it installs. Read the installed `revision`
|
|
627
|
+
(`astrale introspect services.astrale.ai -i <admin-instance> --json`, as recorded at OPS-3
|
|
628
|
+
step 1) and, after step 2, the deployment's `schema.revision` in `services-admin.json`. Equal
|
|
629
|
+
revisions are the same Schema. Different revisions make the move a Schema upgrade: list what
|
|
630
|
+
changed in `services/schema/` between the OPS-3 commit and this one, and let the rehearsal
|
|
631
|
+
(step 3) decide. When admin-beta and `admin` are built from commits whose Services Schema is
|
|
632
|
+
the same (both with the CT16 adoption), only the compiled form can differ.
|
|
633
|
+
1. **Secrets.** Create `.env.admin` (mode 600) as a copy of `.env.admin-beta`. The values must be
|
|
634
|
+
the same, the `SERVICES_SECRET_DIGEST_KEY` above all: the new deployment manages the same hosted
|
|
635
|
+
deployments, in the same dispatch namespace and routing KV, and their recorded secret digests
|
|
636
|
+
depend on that key. No signing identity file: the deploy generates the deployment's key.
|
|
637
|
+
2. **Deploy.** From a clean checkout of `main`, with `CLOUDFLARE_ACCOUNT_ID` and
|
|
638
|
+
`CLOUDFLARE_API_TOKEN` of the platform account set, run
|
|
639
|
+
`pnpm run deploy admin --json > services-admin.json` (no `--`: pnpm passes it on literally and
|
|
640
|
+
`astrale-domain` refuses it as an unknown flag). Check that
|
|
641
|
+
`<url>/.well-known/astrale/release.json` serves the printed release digest and that
|
|
642
|
+
`<url>/.well-known/astrale/deployment.json` serves its record.
|
|
643
|
+
3. **Rehearse** on a disposable instance whose Host runs the Admin Host's Kernel release, whose
|
|
644
|
+
Services pin `admin-beta`, with at least one Service and one hosted deployment: move it (step 4),
|
|
645
|
+
verify it (step 5), roll it back ([Rollback](#rollback-ops-5)) and move it forward again.
|
|
646
|
+
4. **Move** the Admin instance:
|
|
647
|
+
|
|
648
|
+
```sh
|
|
649
|
+
astrale domain install "$(jq -r .url services-admin.json)" \
|
|
650
|
+
--allow-issuer-change=services.astrale.ai -i <admin-instance> --json
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- The frozen Worker's issuer and the deployment's are not on one line, so consent names the
|
|
654
|
+
origin: a bare `--allow-issuer-change` covers only a new deployment of the same line, and the
|
|
655
|
+
install is refused without the origin.
|
|
656
|
+
- The replaced issuer keeps working while its in-flight work drains. Do not pass
|
|
657
|
+
`--revoke-previous`.
|
|
658
|
+
- If the Kernel refuses an installed dependent of Services, install Services together with a
|
|
659
|
+
compatible deployment of that dependent in one grouped command
|
|
660
|
+
(`astrale domain install <services-url> <dependent-url> …`); a refusal installs nothing.
|
|
661
|
+
- If the Kernel answers `4001 DATA_MIGRATION_REQUIRED` ("Schema change requires an explicit
|
|
662
|
+
data migration."), data the Admin instance stores does not fit the deployment's Schema, and
|
|
663
|
+
`reason.details.requirements` names each Class (an admin-beta built without the CT16 adoption
|
|
664
|
+
names `CloudflareDeployment` and `deployment_on_line`). The whole operation is refused: the
|
|
665
|
+
pin does not move, nothing activates and no data changes, although the deployment did receive
|
|
666
|
+
the install request. The Admin instance stays on admin-beta, which keeps answering. Fix
|
|
667
|
+
forward with an `admin` release whose Schema admits the stored data; this repository has no
|
|
668
|
+
data migration. `SCHEMA_UPGRADE_INCOMPATIBLE` (4002) and `SCHEMA_DEPENDENTS_INCOMPATIBLE`
|
|
669
|
+
have the same remedy.
|
|
670
|
+
- On any other refusal, stop. Never uninstall Services on the Admin instance.
|
|
671
|
+
5. **Verify**, with reads only, then one hosted deploy:
|
|
672
|
+
- `astrale introspect services.astrale.ai -i <admin-instance> --json` names the new release,
|
|
673
|
+
pinned exactly, with the deployment URL as issuer;
|
|
674
|
+
- `astrale call /:services.astrale.ai:class.CloudflareDeployment:hosting -i <admin-instance> --json`
|
|
675
|
+
answers the hosting of `svc.eu.beta.astrale.ai`;
|
|
676
|
+
- the Services View lists the Admin instance's Services and their secrets names;
|
|
677
|
+
- a publisher's `astrale-domain deploy <environment>` of a Domain with the astrale adapter into
|
|
678
|
+
the Admin instance still makes, or reuses, its hosted deployment.
|
|
679
|
+
6. **Point bootstrap discovery at the deployment.** Admin's bootstrap discovery for `services`
|
|
680
|
+
moves to this deployment's `release.json` (A11), and Admin is redeployed (OPS-7). Never repoint
|
|
681
|
+
Admin's catalog entry for `services.astrale.ai`: consumer flows install Services by origin
|
|
682
|
+
through it.
|
|
683
|
+
|
|
684
|
+
A later release moves the same way: deploy `admin`, then install its URL, with the step 0 Schema
|
|
685
|
+
check. Its issuer is another deployment URL on the same line, which the bare `--allow-issuer-change`
|
|
686
|
+
covers by the consent rule (CT24); the host proof does not exercise that case, so pass
|
|
687
|
+
`--allow-issuer-change=services.astrale.ai` if the CLI asks for the origin.
|
|
688
|
+
|
|
689
|
+
#### Rollback (OPS-5)
|
|
690
|
+
|
|
691
|
+
Reinstall the frozen Worker with consent. It still serves, because only OPS-6 retires it:
|
|
692
|
+
|
|
693
|
+
```sh
|
|
694
|
+
astrale domain install https://services-admin.beta.astrale.ai \
|
|
695
|
+
--allow-issuer-change=services.astrale.ai --allow-identity-override -i <admin-instance> --json
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
It serves only the legacy `domain.json`, and the origin it declares differs from its host, so the
|
|
699
|
+
identity override applies as well. The Admin instance's Services records stay in its Kernel through
|
|
700
|
+
a move and a rollback, and a rollback is a Schema upgrade too: it is admitted only while
|
|
701
|
+
admin-beta's Schema admits the data written since the move. Once it does not, the Kernel refuses
|
|
702
|
+
as in step 4 and the deployment stays installed; the remedy is then a new `admin` deployment that
|
|
703
|
+
fixes forward, never a redeploy of admin-beta. If step 6 already repointed bootstrap discovery,
|
|
704
|
+
repoint it back too.
|
|
705
|
+
|
|
706
|
+
### Retire admin-beta (OPS-6)
|
|
707
|
+
|
|
708
|
+
Once the Admin instance runs on `admin` and no installation on any Host pins
|
|
709
|
+
`https://services-admin.beta.astrale.ai` (`astrale introspect services.astrale.ai` on each instance
|
|
710
|
+
that installs Services), retire the `services-domain-admin-beta` Worker and its route
|
|
711
|
+
`services-admin.beta.astrale.ai`, and delete its `ASTRALE_SIGNING_IDENTITY` secret, its copy of the
|
|
712
|
+
services.beta key. Keep the issuer host `services.beta.astrale.ai` and the key itself: the frozen
|
|
713
|
+
R1 and services-v2 Workers that 1Pact pins still sign with them. Then D19 deletes the frozen
|
|
714
|
+
`admin-beta` Environment from this repository.
|
|
715
|
+
|
|
716
|
+
### Before publishers deploy
|
|
717
|
+
|
|
718
|
+
The Admin instance's graph is the registry of lines. Before the first publisher deploys:
|
|
719
|
+
|
|
720
|
+
1. This runtime is installed on the Admin instance only, as above; that instance's Kernel issuer is
|
|
721
|
+
`SERVICES_DEPLOYMENT_KERNEL`; and `SERVICES_PLATFORM_OWNER` names the platform organisation before
|
|
722
|
+
its first readable deploy.
|
|
723
|
+
2. Each publisher has an organisation on the Admin instance: an Identity, a Shell User for a single
|
|
724
|
+
publisher or a Shell Group otherwise. The first deploy of an origin in an environment gives that
|
|
725
|
+
line to the organisation it names, and only a [transfer](#transfer-a-line) moves it.
|
|
726
|
+
3. Its members and CI identities are Identities of the Admin instance that act for it through
|
|
727
|
+
Kernel `extends_with`. A Shell Group's `assignMember` writes that Edge for each User or Group it
|
|
728
|
+
assigns. A caller that does not act for the organisation it names is refused before any write.
|
|
729
|
+
4. The Admin instance's graph is backed up with the registry it holds: only it records who holds
|
|
730
|
+
each line, each deployment's record and its secret digests. A restore loses what happened after
|
|
731
|
+
its backup: lines registered since are free for the next organisation that deploys, and secret
|
|
732
|
+
digests may name values older than the ones served.
|
|
733
|
+
|
|
340
734
|
## Plain background Service starter
|
|
341
735
|
|
|
342
736
|
The package also ships an isolated scaffolding binary (the root import remains Schema-only):
|
|
@@ -359,12 +753,13 @@ No tooling dependency is added to the Schema runtime or imported from the packag
|
|
|
359
753
|
|
|
360
754
|
## Domain development
|
|
361
755
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
756
|
+
Services has no development Environment: the [Admin-hosted runtime](#admin-hosted-runtime) is the
|
|
757
|
+
only one it hosts, and Services itself is developed against its tests (the fake Cloudflare account
|
|
758
|
+
and the [real-Kernel suite](tests/kernel/README.md)). `pnpm run deploy admin` makes an immutable
|
|
759
|
+
deployment of this release on the `admin` line of the platform namespace. Deploying never
|
|
760
|
+
installs: installation is always a separate, explicit `astrale domain install <url> -i <instance>`
|
|
761
|
+
on the intended Kernel. There is no watch mode: `astrale-domain dev` serves only direct-mode
|
|
762
|
+
Environments.
|
|
368
763
|
|
|
369
764
|
To validate without deploying:
|
|
370
765
|
|
|
@@ -407,12 +802,12 @@ candidate and the serving Worker. Those exact flags participate in the deploymen
|
|
|
407
802
|
Services must not inject defaults after its computation. The Cloudflare adapter already includes
|
|
408
803
|
public-fetch routing in the generated configuration before building the artifact and its digest.
|
|
409
804
|
|
|
410
|
-
SDK-authored
|
|
805
|
+
SDK-authored Domains should deploy through `@astrale-os/adapter-astrale`, which projects the
|
|
411
806
|
admitted Cloudflare artifact into the revisioned Services request and verifies the live Publication.
|
|
412
|
-
|
|
413
|
-
Services never interprets or
|
|
414
|
-
|
|
415
|
-
deploy provider compute first and then
|
|
807
|
+
Installation is never part of a deploy: install the deployed URL with
|
|
808
|
+
`astrale domain install <url> -i <instance>` as its own step. Services never interprets or caches
|
|
809
|
+
a Domain definition, Publication, Schema revision, issuer, or callable inventory. Manual callers likewise
|
|
810
|
+
deploy provider compute first and then install with the same command.
|
|
416
811
|
|
|
417
812
|
Revisioned deployments may request `readiness: { paths: ['/health'] }`. Services executes these
|
|
418
813
|
bounded local GET paths through its private candidate dispatch binding and requires HTTP 200 before
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-domains/services",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/astrale-os/domains.git",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"registry": "https://registry.npmjs.org/"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@astrale-os/sdk": "0.6.0-beta.
|
|
34
|
+
"@astrale-os/sdk": "0.6.0-beta.14",
|
|
35
35
|
"zod": "4.4.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@astrale-os/adapter-cloudflare": "0.5.0-beta.
|
|
38
|
+
"@astrale-os/adapter-cloudflare": "0.5.0-beta.169",
|
|
39
39
|
"@astrale-os/ox": ">=0.1.4 <1.0.0",
|
|
40
40
|
"@astrale-os/shell": "0.6.0-beta.8",
|
|
41
41
|
"@astrale-os/shell-react": "0.5.0-beta.8",
|
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
"workerd"
|
|
72
72
|
],
|
|
73
73
|
"scripts": {
|
|
74
|
-
"dev": "astrale-domain dev",
|
|
75
74
|
"deploy": "astrale-domain deploy",
|
|
76
75
|
"build": "astrale-domain build",
|
|
77
76
|
"lint": "astrale-domain lint",
|