@astrale-domains/services

中文文档
0.9.6发布于 2026/11/07TypeScriptES Module近 30 天下载 2664

services.astrale.ai hosts Services: Cloudflare Workers an Identity owns. A Service is the stable identity of one project in one environment. Every deploy adds an immutable deployment of it, and the Service's URL serves the one it currently points at, li

原始使用文档
文档目录11 节
  1. Model
  2. Owner and authority
  3. A Service without an owner
  4. Methods
  5. Deploying
  6. Routing and crons
  7. Secrets
  8. Rollback and retirement
  9. Generic Services
  10. Admin-hosted runtime
  11. Domain development

Services

services.astrale.ai hosts Services: Cloudflare Workers an Identity owns. A Service is the stable identity of one project in one environment. Every deploy adds an immutable deployment of it, and the Service's URL serves the one it currently points at, like a project and its deployments.

Services is agnostic of what it hosts. An Astrale Domain release (astrale-domain deploy with @astrale-os/adapter-astrale) and a generic Worker (a cron job, an API, a create-astrale-service project) are the same thing to it: a Worker build, the configuration it freezes, the secrets it binds and, when it publishes one, an opaque record. Every Domain fact (origin, release, Schema revision, Publication, issuer) is the deployer's. Provider protocols stay behind a neutral SDK Integration, and the graph keeps only admitted lifecycle evidence.

Model

  • Service / CloudflareService: one (project, environment). A project is any name (PROJECT_NAME of @astrale-os/sdk/deployment/worker); a Domain deployer names its Domain's origin. Its name is the opaque 16-character serviceName(project, environment), unique among Services, the DNS label of url, https://<name>.<routing domain>. state is active, or deleting while its deletion retires its deployments.
  • service_owned_by: a Service has at most one owner, an Identity: a user, a group or a service account. It is chosen once, when the first deploy creates the Service, and changes only by transfer. The Kernel cannot restrict the deletion of an Identity (kernel#1498): deleting the owner detaches the Edge, and the Service survives without an owner (below).
  • Deployment / CloudflareDeployment: one immutable release of a Service, served by one Workers for Platforms script at its own URL, https://<label>.<routing domain>, where label is <name>-<content16> and the content covers the build digest and the configuration digest. It keeps generic facts only: buildDigest, its configuration (WorkerConfigurationV1) and configurationDigest, the record it publishes if any (opaque bytes and their media type), its key id when it asked for a signing identity, the script's code hash, the keyed digests of its secrets, and its lifecycle: deploying → active → retiring → retired, or failed. A retired deployment is never served again.
  • deployment_of: the Service a deployment is a version of. A deployment has no owner of its own: it belongs to whoever owns its Service.
  • current_deployment: the deployment the Service's URL serves and its crons run on. Promoting another deployment moves this Edge; a rollback is the promotion of an older one.

Secret values stay write-only at Cloudflare and never enter a Mutation or the graph. Schedules are provider state. Logs are read from the provider when asked.

Owner and authority

One Policy decides everything: OwnService, the subject is the owner of the Service. The Kernel evaluates it over the caller and every Identity the caller reaches through extends_with, with deny-wins. Nothing is selected at check time: a caller in no group reaches its own Services, a caller in three groups also reaches the Services of each.

  • A deploy that creates a Service makes the caller its owner, or the owner it names when the caller is or acts as that Identity (ActAs).
  • A service account deploys a Service when it owns it, or when it is a member of the group that owns it (Group.assignMember writes the extends_with Edge). A member has every right on every Service of the group, deletion included.
  • OwnDeployment gives a deployment to the owner of its Service.

Every Method is authenticated. Each handler asks the Kernel Identity.can for the caller on its own session before it reads anything: a caller who does not own the Service finds none (SERVICE_NOT_FOUND, DEPLOYMENT_NOT_FOUND), and a deploy of another owner's Service is refused service-owned-elsewhere.

list reads Services under the Services Domain credential and keeps those the Kernel answers OwnService for, in batches: a caller-mode read does not reach the Identity end of service_owned_by for an owner the caller reaches through extends_with (kernel#1138).

A Service without an owner

Once its owner Identity is deleted, a Service is nobody's: no caller satisfies OwnService on it, not even the Kernel root, whose Identity.can answers are factual. Only an authority beyond ownership still reaches it, such as the root writing the graph directly. The handlers treat it as a declared state:

  • Its Methods find no Service (SERVICE_NOT_FOUND), as for any Service the caller does not own.
  • A deploy of its project and environment is refused service-owned-elsewhere, with a message that it has no owner and must be transferred to one first.
  • list never lists it.
  • A transfer or a delete whose caller proved it owned the Service before its owner was deleted completes: the transfer gives the Service the owner it names, and the deletion deletes it without an owner Edge. Each Mutation fences on the owner it read, or on the Service still having none.

Methods

Call them on the Admin instance (astrale call ... --admin). Their wire contract is @astrale-os/sdk/deployment/worker's.

MethodOnWhat it does
deployCloudflareService (static)Hosts one Worker release (WorkerDeploymentRequestV1) as a deployment of the Service of its project and environment, creating the Service on its first deploy, and by default (promote absent or true) makes the Service serve it. Answers WorkerDeploymentResponseV1.
hostingCloudflareService (static)The routing domain Services and deployments are addressed under (WorkerHostingV1); names are always opaque.
listCloudflareService (static)The deployments of the Services the caller owns (WorkerDeploymentSummaryV1), narrowed to a project and an environment when given, each with current and the record it publishes.
promoteCloudflareServicePoints the Service at one of its active deployments, named by its label.
transferCloudflareServiceGives the Service to another owner the caller is or acts as.
setSecret, secretsCloudflareServiceRewrites one declared secret of the current deployment in place; lists the names it binds, never a value.
setSchedule, schedulesCloudflareServiceReplaces or reads the Service's UTC cron set; each cron fires on the current deployment.
logsCloudflareServiceRecent logs of the current deployment, or of one deployment named by label.
deleteCloudflareServiceStops routing the Service, retires every deployment of it, then deletes it and frees its name.
retireCloudflareDeploymentWithdraws one deployment for good. The current deployment is never retired alone.

Deploying

deploy is a non-durable Action: its input holds the secrets, and no Execution, step result or journal entry keeps them.

  1. Before any read, the SDK's decodeWorkerDeploymentRequest admits the request: every file decoded within its bounds, the configuration canonical, a record exactly when the configuration asks for one, the secrets exactly the names it declares. It recomputes the build digest from the uploaded bytes (build-mismatch) and the label from the Service name, the build and the configuration (address-mismatch); anything else is invalid-deployment. A configuration that asks for the router where this environment has none is hosting-unavailable.
  2. It reads the Service of the name under the Services Domain credential and proves the caller's authority on the caller's own session: OwnService on an existing Service, ActAs on the owner it names for a new one.
  3. A new label is created under a graph lease. The provider uploads it create-only with the host bindings (its URL as WORKER_URL, the recomputed ASTRALE_BUILD_DIGEST, the version metadata ASTRALE_VERSION_METADATA), the configuration's variables, a signing key generated for this deployment alone (ASTRALE_SIGNING_IDENTITY) only when it asks signingIdentity, the router (ROUTER) only when it asks router, and the request's secrets. When the request names readiness paths, each must answer 200 from the uploaded version (x-astrale-worker-version); none skips the probe. The label is then activated through the routing KV record:<label> with the record the deployment publishes, served byte for byte with its media type at /.well-known/astrale/deployment.json, or a placeholder when it publishes none. The graph records it active with its code hash, its key id when it has one, and keyed HMAC-SHA256 digests of its secrets.
  4. The same release again is the same label: deploy answers reused. Its secrets are compared with the recorded digests: unchanged, or rewritten in place on that deployment alone, updated.
  5. When it promotes, current_deployment moves to the deployment, fenced on what the Service served, and the routing KV current:<name> names its label. A routing write that failed is answered service-unavailable; deploying or promoting again converges.

The names the host binds itself (WORKER_HOST_BINDINGS: WORKER_URL, ASTRALE_BUILD_DIGEST, ASTRALE_VERSION_METADATA, ASTRALE_SIGNING_IDENTITY, ROUTER, ASSETS) are never a variable or a secret of the request.

  • A Domain is deployed by astrale-domain deploy <environment> with astrale() of @astrale-os/adapter-astrale: its project is the Domain's origin, its build manifest members are the Domain's (so ASTRALE_BUILD_DIGEST is the sealed release's), its execution settings travel as the ASTRALE_SETTINGS variable, it publishes its DeploymentRecordV1 and asks for a signing identity, and readiness probes the release's documents. The adapter, not Services, verifies the release served and installs it deliberately.
  • Any other Worker is deployed with @astrale-os/adapter-astrale/services (readWorkerDirectory, deployWorker, callService), as the projects create-astrale-service generates do.

Routing and crons

The Services dispatcher is the hosting kit's dispatcher (@astrale-os/adapter-cloudflare/hosting), with host-based deployment routing only; retired tenant routing is no longer provisioned. It serves one routing domain:

  • https://<label>.<routing domain> serves that deployment: record:<label> activates it, and retirement:<label> answers DEPLOYMENT_RETIRED (410). It answers the deployment's record at /.well-known/astrale/deployment.json itself; a deployment that publishes none answers that path.
  • https://<name>.<routing domain> serves the deployment current:<name> names, through the same path, so a retired deployment is never served through its Service either.
  • Every minute the dispatcher reads the cron:<name> keys and posts each due cron to the current deployment of that Service: POST https://cron.internal/__scheduled with the expression in x-astrale-cron and the minute it fired for, in epoch milliseconds, in x-astrale-scheduled-time, within a 10-second deadline. A non-2xx answer is logged. The public never reaches /__scheduled: the dispatcher refuses it in every spelling (case, repeated or trailing slashes, percent-encoding) before any lookup. A run is not exactly-once: a job must be safe to repeat.

Secrets

Services never keeps a value. A deploy binds the request's secrets to the new deployment only; every other deployment keeps its own. setSecret rewrites one name the current deployment's configuration declares, in place, under the deployment's graph lease: it compares the keyed digests, writes only changed values, forgets the digests of the names it writes first and records the new ones once every write succeeded. The provider serves a new value a few seconds later. A new name, or removing one, is a new release; a signing key never rotates.

Never pass a secret value in argv. Write the input to a file only you can read, outside the repository:

{ "name": "API_TOKEN", "value": "<the new value>" }
astrale call "@$SERVICE::services.astrale.ai:class.CloudflareService.method.setSecret" \
  --admin -d @rotate.json --json

Put the new value in the environment's secrets as well: the next deploy binds the values it carries.

Rollback and retirement

astrale call /:services.astrale.ai:class.CloudflareService:list \
  project=agencies.example.com environment=production --admin --json
astrale call "@$SERVICE::services.astrale.ai:class.CloudflareService.method.promote" \
  label=$PREVIOUS_LABEL --admin --json

retire withdraws one deployment in two phases: the first call records it retiring and writes its tombstone, and a call after retryAfterMs (once every dispatcher reads the tombstone, about 65 seconds) deletes its script and key and answers retired.

astrale call "@$DEPLOYMENT::services.astrale.ai:class.CloudflareDeployment.method.retire" \
  --admin --json

delete marks the Service deleting, removes current:<name> and cron:<name>, and retires every deployment. It answers deleting with retryAfterMs until the last one is retired, then deletes the Service with its deployments in one Mutation and answers deleted. The name is then free again.

Generic Services

The package ships an isolated scaffolding binary (the root import remains Schema-only):

npm exec --package @astrale-domains/services -- create-astrale-service nightly-report
cd nightly-report
npm install
# Edit service.json and src/job.js; put the secret values in .env.production.
npm run deploy -- production

The generator creates local source only and refuses an existing destination: a plain Worker (src/index.js answering GET /health and running src/job.js on POST /__scheduled), an optional ES256 account session to call an Astrale Kernel (src/account.js), service.json (the project, the Workers runtime, the variables, the secret names, readiness paths and crons) and its build (esbuild) and deploy scripts. npm run deploy -- <environment> builds it, deploys it through @astrale-os/adapter-astrale/services as one immutable deployment promoted by default (--no-promote deploys beside the current one), then replaces the Service's crons with service.json's. It installs, registers, grants and schedules nothing else. The generated README explains promotion and rollback, secrets, crons, logs and deletion. The scaffold lives in tooling/scaffold; pnpm package copies it to dist/scaffold.

Admin-hosted runtime

Services are hosted for the Admin instance's Kernel alone: its graph is the registry of Service names, while the deployments namespace and the routing KV are shared. A Domain Worker serves every Kernel that installs it; any other Kernel finds hosting unavailable before anything is read.

The admin Environment sets the plain vars ASTRALE_ROUTER_WORKER, SERVICES_DEPLOYMENT_HOST (the routing domain) and SERVICES_DEPLOYMENT_KERNEL (the Admin Kernel's issuer URL). These vars are the only declaration of hosting and also bind SERVICE_DEPLOYMENTS. Its secrets file, .env.admin, sets SERVICES_SECRET_DIGEST_KEY (at least 32 UTF-8 bytes), CLOUDFLARE_DISPATCH_NAMESPACE and CLOUDFLARE_SCHEDULE_KV_ID; the configuration refuses a file without them or one that sets a hosting var.

.env.admin.tpl is that file with each secret as a 1Password reference. scripts/with-secrets.sh admin -- <command> resolves it for one command: .env.admin (0600) exists while the command runs, its values are in the command's environment, and the file is removed afterwards. A resolved .env.admin left on disk is refused rather than overwritten.

pnpm run deploy admin makes one immutable deployment of this release in the platform dispatch namespace. Deploying never installs: install the deployed URL with astrale domain install <url> -i <instance>. The Cloudflare Provider and its operator tooling are built on the hosting kit: provisioning both dispatchers (the Services environment's and the platform namespace's), audits, platform retirement and secret rotation are described in providers/cloudflare/operator/README.md.

Domain development

Services is developed against its tests: the hosting kit's fake Cloudflare account (@astrale-os/adapter-cloudflare/hosting/testing, running the dispatchers as uploaded, and the scaffolded Worker itself) and the real-Kernel suite.

pnpm typecheck
pnpm test
pnpm lint
pnpm frontend:build
pnpm build
pnpm package

版本与发布记录

@astrale-domains/services 共发布 39 个版本,首次发布于 2026/08/26;过去 12 个月发布了 39 个版本。

版本发布日期距今
0.9.6latest2026/11/07今天
0.9.52026/11/052 天前
0.9.42026/11/044 天前
0.9.32026/11/034 天前
0.9.22026/11/025 天前
0.9.12026/10/1920 天前

关于 @astrale-domains/services 的常见问题

@astrale-domains/services 是什么?

services.astrale.ai hosts Services: Cloudflare Workers an Identity owns. A Service is the stable identity of one project in one environment. Every deploy adds an immutable deployment of it, and the Service's URL serves the one it currently points at, li

如何安装 @astrale-domains/services?

在项目目录运行 npm install @astrale-domains/services,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 create-astrale-service,可以用 npx create-astrale-service 直接运行。

@astrale-domains/services 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@astrale-domains/services 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@astrale-domains/services 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@astrale-domains/services 可以商用吗?

@astrale-domains/services 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@astrale-domains/services 的下载量有多少?

根据 npm 官方下载统计,@astrale-domains/services 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,664 次,日均约 89 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@astrale-domains/services 还在维护吗?

最新版本 0.9.6 发布于 2026/11/07(今天);至今共发布 39 个版本,其中过去 12 个月发布了 39 个;首次发布于 2026/08/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。