@astrale-os/sdk 0.6.0-beta.20 → 0.6.0-beta.22

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 (31) hide show
  1. package/README.md +0 -4
  2. package/dist/application/workflow/durable.d.ts +12 -8
  3. package/dist/deployment/adapter/adapter.d.ts +8 -1
  4. package/dist/deployment/adapter/define.d.ts +1 -1
  5. package/dist/deployment/adapter/define.js +40 -1
  6. package/dist/deployment/adapter/release/command.d.ts +33 -0
  7. package/dist/deployment/adapter/release/command.js +1 -0
  8. package/dist/deployment/adapter/release/index.d.ts +1 -0
  9. package/dist/deployment/adapter/release/list.d.ts +11 -10
  10. package/dist/deployment/address/index.d.ts +1 -1
  11. package/dist/deployment/address/index.js +0 -1
  12. package/dist/deployment/address/summary.d.ts +17 -40
  13. package/dist/deployment/address/summary.js +1 -117
  14. package/dist/deployment/index.d.ts +1 -1
  15. package/dist/execution/workflows/run.js +18 -6
  16. package/dist/tooling/cli/adapter-command.d.ts +35 -0
  17. package/dist/tooling/cli/adapter-command.js +66 -0
  18. package/dist/tooling/cli/arguments.d.ts +7 -1
  19. package/dist/tooling/cli/arguments.js +53 -1
  20. package/dist/tooling/cli/help.js +11 -8
  21. package/dist/tooling/cli/index.d.ts +4 -2
  22. package/dist/tooling/cli/index.js +3 -2
  23. package/dist/tooling/cli/list.d.ts +5 -5
  24. package/dist/tooling/cli/list.js +16 -14
  25. package/dist/tooling/cli/orchestrate.js +18 -0
  26. package/dist/tooling/cli/publish/index.d.ts +4 -5
  27. package/dist/tooling/cli/publish/index.js +9 -28
  28. package/dist/tooling/cli/registry/client.d.ts +0 -13
  29. package/dist/tooling/cli/registry/client.js +4 -9
  30. package/dist/tooling/cli/registry/index.d.ts +1 -1
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -345,10 +345,6 @@ did not derive.
345
345
  - **Record.** `acceptDeploymentRecord` admits a `DeploymentRecordV1` only when its provider
346
346
  script is the label that its own fields derive. A host admits the record a deploy declares with
347
347
  it before storing it, so every stored record decodes.
348
- - **Platform summary.** `acceptPlatformDeploymentSummary` admits the `summary:<label>` a platform
349
- dispatch namespace keeps beside `record:<label>`: the deployment's `DeploymentSummaryV1` without
350
- its record, owned by `platform`, named by its label, with no provider version id and no
351
- `lastCallAt` or `expiresAt`. `encodePlatformDeploymentSummary` gives its one canonical text.
352
348
 
353
349
  ```ts
354
350
  import {
@@ -36,25 +36,29 @@ export interface DurableExecution {
36
36
  */
37
37
  export interface DurableStepAuthority<DomainValue extends Domain = Domain> {
38
38
  /**
39
- * Kernel Session acting as the Domain. Each `invoke` without an explicit `idempotencyKey` carries
40
- * one derived from the Execution, the Step and its position, so a retried Step reaches the same
41
- * durable child Execution.
39
+ * Kernel Session acting as the Domain. Each `invoke` of a durable callable without an explicit
40
+ * `idempotencyKey` carries one derived from the Execution, the Step and its position, so a
41
+ * retried Step reaches the same durable child Execution. An `invoke` of any other callable
42
+ * carries no key, which the Kernel would refuse, and runs again when the Step is retried.
42
43
  */
43
44
  readonly kernel: BoundClientSession;
44
45
  readonly query: QueryExecutor<DomainValue>;
45
- /** Each Mutation carries a key derived from the Execution, the Step and its position. */
46
+ /**
47
+ * Each Mutation carries a key derived from the Execution, the Step and its position. The Kernel
48
+ * commits a receipt with the write and answers a retried Step with its first result.
49
+ */
46
50
  readonly mutate: MutationExecutor<DomainValue>;
47
51
  }
48
52
  export interface DurableStep<DomainValue extends Domain = Domain> {
49
53
  /**
50
54
  * Run `execute` once and journal its portable output; a replay returns the journaled output. A
51
- * Step may run again when its outcome was not journaled, so its effects must be idempotent.
55
+ * Step may run again when its outcome was not journaled: its Mutations and durable calls are
56
+ * answered from their keys, and any other effect must be idempotent.
52
57
  */
53
58
  run<Output>(id: string, execute: (authority: DurableStepAuthority<DomainValue>) => Output | Promise<Output>): Promise<Output>;
54
59
  /**
55
- * One journaled Mutation. Give it a zero-match precondition on a natural key (for example the
56
- * Execution id) and map that failure in `reject`, so a Step replayed after its commit finds its
57
- * own effect instead of repeating it.
60
+ * One journaled Mutation. A Step replayed after its commit receives the result of that commit
61
+ * from the Kernel's receipt instead of writing again.
58
62
  */
59
63
  mutate<Input, Output>(id: string, mutation: Mutation<DomainValue, Input, Output>, input: Input): Promise<Output>;
60
64
  }
@@ -1,7 +1,7 @@
1
1
  import type { FrozenConfigurationV1 } from '../address/index.js';
2
2
  import type { Bundler } from './bundle/index.js';
3
3
  import type { ArtifactSource, DeployContext, DeployResult, PrepareContext } from './legacy/index.js';
4
- import type { ConfigureContext, ListContext, ListedDeployment, Placement, PlacementContext, ReleaseDeployContext, ReleaseDeployResult, RetainContext } from './release/index.js';
4
+ import type { AdapterCommand, ConfigureContext, ListContext, ListedDeployment, Placement, PlacementContext, ReleaseDeployContext, ReleaseDeployResult, RetainContext } from './release/index.js';
5
5
  /** What `admit` receives when `defineProject` binds an adapter to one Environment. */
6
6
  export interface AdapterEnvironmentContext {
7
7
  readonly environment: string;
@@ -44,6 +44,12 @@ export interface Adapter<Parameters extends object = object> {
44
44
  * transient. Absent: the registry marks it (Admin's Services).
45
45
  */
46
46
  retain?(parameters: Parameters, context: RetainContext): Promise<void>;
47
+ /**
48
+ * Provider commands, by name: `astrale-domain <adapter name> <command> <environment>` runs one
49
+ * on an Environment this adapter deploys (adapter-cloudflare's `plan` and `init` provision the
50
+ * host on the operator's own account). Every effect is the adapter's; the CLI only routes.
51
+ */
52
+ readonly commands?: Readonly<Record<string, AdapterCommand<Parameters>>>;
47
53
  /**
48
54
  * @deprecated Legacy direct-mode path (CT33) only: place a build at a stable target. Replacement:
49
55
  * `bundler`, `configure` and `placement`. Deleted with the legacy path (D10).
@@ -67,6 +73,7 @@ export interface AdapterInput<Parameters extends object> {
67
73
  deployRelease(parameters: Parameters, context: ReleaseDeployContext): Promise<ReleaseDeployResult>;
68
74
  list?(parameters: Parameters, context: ListContext): Promise<readonly ListedDeployment[]>;
69
75
  retain?(parameters: Parameters, context: RetainContext): Promise<void>;
76
+ readonly commands?: Readonly<Record<string, AdapterCommand<Parameters>>>;
70
77
  /** @deprecated Legacy direct-mode path (CT33) only. Replacement: `configure`, `placement`. */
71
78
  prepare?(parameters: Parameters, context: PrepareContext): Promise<ArtifactSource>;
72
79
  /** @deprecated Legacy direct-mode path (CT33) only. Replacement: `deployRelease`. */
@@ -3,7 +3,7 @@ import type { Adapter, AdapterInput } from './adapter.js';
3
3
  import type { LegacyAdapter, LegacyAdapterInput } from './legacy/index.js';
4
4
  /**
5
5
  * Capture one receiver-bound provider adapter (adapter interface v2): a required `bundler`,
6
- * `configure`, `placement` and `deployRelease`, optionally `list` and `retain`, and `prepare` with
6
+ * `configure`, `placement` and `deployRelease`, optionally `list`, `retain` and provider `commands`, and `prepare` with
7
7
  * `deploy` only while it also serves the legacy direct-mode path.
8
8
  */
9
9
  export declare function defineAdapter<Parameters extends object>(input: AdapterInput<Parameters>): Adapter<Parameters>;
@@ -12,6 +12,7 @@ const FIELDS = [
12
12
  'deployRelease',
13
13
  'list',
14
14
  'retain',
15
+ 'commands',
15
16
  'prepare',
16
17
  'deploy',
17
18
  'secretsFile',
@@ -22,7 +23,9 @@ const OPTIONAL_RELEASE_OPERATIONS = ['list', 'retain'];
22
23
  /** The operations of the legacy direct-mode path. */
23
24
  const STABLE_TARGET_OPERATIONS = ['prepare', 'deploy'];
24
25
  /** Every operation an adapter may declare, in the order an admitted adapter lists them. */
25
- const OPERATIONS = FIELDS.filter((key) => key !== 'name' && key !== 'version' && key !== 'bundler');
26
+ const OPERATIONS = FIELDS.filter((key) => key !== 'name' && key !== 'version' && key !== 'bundler' && key !== 'commands');
27
+ /** A provider command name, `astrale-domain <adapter> <command>`, and an option name. */
28
+ const COMMAND_NAME = /^[a-z][a-z0-9-]{0,31}$/u;
26
29
  export function defineAdapter(input) {
27
30
  const value = record(input, 'Adapter definition');
28
31
  if (Reflect.ownKeys(value).some((key) => !FIELDS.includes(key))) {
@@ -47,12 +50,16 @@ export function defineAdapter(input) {
47
50
  key,
48
51
  capture(receiver, key),
49
52
  ]));
53
+ if (value.commands !== undefined && !release) {
54
+ throw new TypeError(`Adapter ${name} declares commands without adapter interface v2.`);
55
+ }
50
56
  const adapter = Object.freeze({
51
57
  kind: 'adapter',
52
58
  name,
53
59
  version,
54
60
  bundler,
55
61
  ...operations,
62
+ ...(value.commands === undefined ? {} : { commands: admitCommands(name, value.commands) }),
56
63
  });
57
64
  admittedAdapters.add(adapter);
58
65
  return adapter;
@@ -131,6 +138,38 @@ function missingBundler(name) {
131
138
  'to the adapter as its Bundle (a Cloudflare Worker adapter declares ' +
132
139
  '`cloudflareWorkerBundler` from `@astrale-os/adapter-cloudflare/build`).');
133
140
  }
141
+ /**
142
+ * Capture the provider commands of one adapter: each a lower-case name, a one-line `summary`,
143
+ * optional `options` (name to one line of help) and a receiver-bound `run`.
144
+ */
145
+ function admitCommands(adapter, input) {
146
+ const commands = record(input, `Adapter ${adapter} commands`);
147
+ const admitted = {};
148
+ for (const name of Reflect.ownKeys(commands)) {
149
+ if (typeof name !== 'string' || !COMMAND_NAME.test(name)) {
150
+ throw new TypeError(`Adapter ${adapter} declares an invalid command name.`);
151
+ }
152
+ const command = record(commands[name], `Adapter ${adapter} command ${name}`);
153
+ const options = command.options === undefined ? {} : record(command.options, 'Command options');
154
+ if (Reflect.ownKeys(command).some((key) => key !== 'summary' && key !== 'options' && key !== 'run') ||
155
+ typeof command.summary !== 'string' ||
156
+ command.summary.length === 0 ||
157
+ typeof command.run !== 'function' ||
158
+ Reflect.ownKeys(options).some((key) => typeof key !== 'string' ||
159
+ !COMMAND_NAME.test(key) ||
160
+ key === 'json' ||
161
+ typeof options[key] !== 'string')) {
162
+ throw new TypeError(`Adapter ${adapter} command ${name} must declare a summary, string options other than ` +
163
+ 'json and run.');
164
+ }
165
+ admitted[name] = Object.freeze({
166
+ summary: command.summary,
167
+ options: Object.freeze({ ...options }),
168
+ run: command.run.bind(command),
169
+ });
170
+ }
171
+ return Object.freeze(admitted);
172
+ }
134
173
  function invalidFields() {
135
174
  throw new TypeError('Adapter definition contains missing or unknown fields.');
136
175
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * One command an adapter declares for its provider (`astrale-domain <adapter> <command>
3
+ * <environment>`), such as adapter-cloudflare's `plan` and `init`, which provision the host on the
4
+ * operator's own account. The CLI only routes: it loads the Project, selects the Environment, checks
5
+ * that its adapter is the one named and declares the command, and admits the options the command
6
+ * declares; everything the command does is the adapter's.
7
+ */
8
+ export interface AdapterCommand<Parameters extends object = object> {
9
+ /** One line: what the command does, for help. */
10
+ readonly summary: string;
11
+ /** The options it takes, `--<name> <value>`, each with one line of help. */
12
+ readonly options?: Readonly<Record<string, string>>;
13
+ run(parameters: Parameters, context: AdapterCommandContext): Promise<AdapterCommandResult>;
14
+ }
15
+ /** What an adapter command receives. */
16
+ export interface AdapterCommandContext {
17
+ readonly environment: string;
18
+ /** Parent-owned lifecycle cancellation. */
19
+ readonly signal: AbortSignal;
20
+ /** The options given, each one the command declares. */
21
+ readonly options: Readonly<Record<string, string>>;
22
+ }
23
+ /** What an adapter command answers; a command that cannot run rejects instead. */
24
+ export interface AdapterCommandResult {
25
+ /** 0 done; 1 refused or blocked (the command ran, and says why in `warnings`). */
26
+ readonly exitCode: 0 | 1;
27
+ /** Lines for a person, printed on stdout without `--json`. */
28
+ readonly lines: readonly string[];
29
+ /** Lines for stderr, printed in either case. */
30
+ readonly warnings?: readonly string[];
31
+ /** The JSON-serializable result `--json` prints. */
32
+ readonly result: unknown;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
1
  export type { ConfigureContext, ListContext, Placement, PlacementContext, ReleaseDeployContext, RetainContext, } from './context.js';
2
+ export type { AdapterCommand, AdapterCommandContext, AdapterCommandResult } from './command.js';
2
3
  export type { DeploymentCallTarget, ListedDeployment } from './list.js';
3
4
  export type { DeploymentSecretsState, FailedReleaseDeployResult, MutationKnowledge, ReadyReleaseDeployResult, ReleaseDeploymentPhase, ReleaseDeployResult, } from './result.js';
@@ -11,8 +11,9 @@ import type { DeploymentSummaryV1 } from '../../address/index.js';
11
11
  * and the deployment it currently serves: `promote` (this deployment's label rolls the Service
12
12
  * back or forward to it), `setSecret` and `secrets`, `setSchedule` and `schedules`, `logs`,
13
13
  * `transfer` and `delete`.
14
- * - `namespace`: a deployment of a platform dispatch namespace, which has no Services node: the
15
- * namespace and the script the per-script tooling of that namespace takes.
14
+ * - `namespace`: a deployment of a Cloudflare dispatch namespace (adapter-cloudflare's namespace
15
+ * mode), which has no Services node: the namespace and the script, which the hosting kit
16
+ * (`@astrale-os/adapter-cloudflare/hosting`) retires, points a name at or schedules.
16
17
  */
17
18
  export type DeploymentCallTarget = {
18
19
  readonly kind: 'services';
@@ -27,11 +28,11 @@ export type DeploymentCallTarget = {
27
28
  };
28
29
  /**
29
30
  * One deployment as an adapter's `list` returns it: what its host knows of it, its record as the
30
- * host stores it, and where it is called. `lastCallAt`, and a preview's `expiresAt`, are as of the
31
- * listing: an adapter whose host records calls only now and then reads the calls counted since, and
32
- * one that cannot read them for want of a permission omits both rather than print stale values.
33
- * Admin's Services keeps every deployment until it is retired and counts no call: its deployments
34
- * are `published`, `withdrawn` once retiring, and carry neither clock.
31
+ * host stores it, and where it is called. Admin's Services keeps every deployment until it is
32
+ * retired and counts no call: its deployments are `published` and `withdrawn` once retiring. A
33
+ * Cloudflare dispatch namespace keeps no retention: its rows carry none, and carry their creation
34
+ * instant and key only while their script exists. No first-party adapter reports a last call or an
35
+ * expiry any more; the CLI still prints one an older adapter reports.
35
36
  *
36
37
  * The adapter does not admit the record. The listing admits it with `acceptDeploymentRecord`, and a
37
38
  * record it refuses (one a host on an older SDK still admits, say) never fails the listing: that
@@ -40,13 +41,13 @@ export type DeploymentCallTarget = {
40
41
  export interface ListedDeployment extends Omit<DeploymentSummaryV1, 'record'> {
41
42
  /**
42
43
  * The record as the host stores it, unadmitted, whatever value that is (JSON `null` included).
43
- * Absent when the host keeps no record of the deployment: a platform namespace keeps none until
44
- * it activates the deployment.
44
+ * Absent when the host keeps no record of the deployment: a dispatch namespace keeps none for a
45
+ * label it only retired.
45
46
  */
46
47
  readonly record?: unknown;
47
48
  /**
48
49
  * Whether the stable URL of the deployment's Service serves it. Present only where the host has
49
- * Services (Admin's Services); a platform namespace has none.
50
+ * Services (Admin's Services); a dispatch namespace has none.
50
51
  */
51
52
  readonly current?: boolean;
52
53
  readonly callTarget: DeploymentCallTarget;
@@ -2,4 +2,4 @@ export { configurationDigest, frozenConfiguration, FROZEN_CONFIGURATION_FORMAT,
2
2
  export { DEPLOYMENT_LABEL, deploymentContent, deploymentLabel, deploymentUrl, parseDeploymentLabel, type DeploymentLabelParts, } from './label.js';
3
3
  export { deploymentLine, lineFingerprint, readablePart, type DeploymentLineInput, type LineAddressing, } from './line.js';
4
4
  export { acceptDeploymentRecord, DEPLOYMENT_RECORD_FORMAT, DEPLOYMENT_RECORD_VERSION, type DeploymentCommitV1, type DeploymentRecordV1, } from './record.js';
5
- export { acceptPlatformDeploymentSummary, encodePlatformDeploymentSummary, PLATFORM_OWNER, type DeploymentSummaryV1, type PlatformDeploymentSummaryV1, } from './summary.js';
5
+ export type { DeploymentSummaryV1 } from './summary.js';
@@ -2,4 +2,3 @@ export { configurationDigest, frozenConfiguration, FROZEN_CONFIGURATION_FORMAT,
2
2
  export { DEPLOYMENT_LABEL, deploymentContent, deploymentLabel, deploymentUrl, parseDeploymentLabel, } from './label.js';
3
3
  export { deploymentLine, lineFingerprint, readablePart, } from './line.js';
4
4
  export { acceptDeploymentRecord, DEPLOYMENT_RECORD_FORMAT, DEPLOYMENT_RECORD_VERSION, } from './record.js';
5
- export { acceptPlatformDeploymentSummary, encodePlatformDeploymentSummary, PLATFORM_OWNER, } from './summary.js';
@@ -1,57 +1,34 @@
1
1
  import type { DeploymentRecordV1 } from './record.js';
2
2
  /**
3
3
  * What a host knows of one immutable deployment beside the record its deployer declared: where it
4
- * answers, who owns its line, its lifecycle and its retention. It names a deployment by its label
5
- * and URL, never by a provider version id. An adapter's `list` returns these; the host owns every
6
- * field but `record`, which it stores as declared.
4
+ * answers, who owns its line and its lifecycle. It names a deployment by its label and URL, never
5
+ * by a provider version id. An adapter's `list` returns these; the host owns every field but
6
+ * `record`, which it stores as declared.
7
7
  */
8
8
  export interface DeploymentSummaryV1 {
9
- /** The host's own identifier of the deployment (a graph node id on Admin's Services). */
9
+ /** The host's own identifier of the deployment (a graph node id on Admin's Services, the label on a dispatch namespace). */
10
10
  readonly id: string;
11
11
  readonly url: string;
12
12
  readonly label: string;
13
13
  readonly line: string;
14
- /** The Identity owning the line: a node path on Admin's Services, `platform` on a platform namespace. */
14
+ /** Who owns the line: a node path on Admin's Services, the dispatch namespace's name on Cloudflare. */
15
15
  readonly owner: string;
16
16
  readonly state: 'deploying' | 'active' | 'retiring' | 'retired' | 'failed';
17
- readonly retention: 'preview' | 'published';
18
- readonly createdAt: string;
17
+ /**
18
+ * `published` on Admin's Services, which keeps every deployment until it is retired. Absent on a
19
+ * dispatch namespace, which keeps no retention.
20
+ */
21
+ readonly retention?: 'preview' | 'published';
22
+ /** When the host created it; absent when the host no longer knows (its script is gone). */
23
+ readonly createdAt?: string;
24
+ /** An older adapter's report of the last call; no first-party adapter reports it any more. */
19
25
  readonly lastCallAt?: string;
20
- /** A preview's: 30 days after its activation or its last call, whichever is later (AM-123). */
26
+ /** An older adapter's preview expiry; no first-party adapter reports it any more. */
21
27
  readonly expiresAt?: string;
22
28
  readonly retiredAt?: string;
29
+ /** `withdrawn` once its owner retired it (`expired` only from an older adapter). */
23
30
  readonly retirement?: 'expired' | 'withdrawn';
24
- readonly signingKeyId: string;
31
+ /** The RFC 7638 thumbprint of its key; absent when the host no longer knows (its script is gone). */
32
+ readonly signingKeyId?: string;
25
33
  readonly record: DeploymentRecordV1;
26
34
  }
27
- /** The owner every summary of a platform namespace names: the platform holds all of its lines. */
28
- export declare const PLATFORM_OWNER = "platform";
29
- /**
30
- * `summary:<label>` in the routing KV of a platform dispatch namespace (CT20, AM-131): the
31
- * `DeploymentSummaryV1` of one deployment without its record, which stays at `record:<label>`.
32
- *
33
- * Its writers are the deployer (`cloudflare({ namespace })`), which writes it at activation,
34
- * before `record:<label>`, names in it the new key of a script it recreates under the label after
35
- * an operator deleted it, and marks it published (`retain`), and the platform retirement tooling
36
- * (SV6), which records a withdrawal. So it names the deployment by its label (`id` is the label),
37
- * is owned by `platform`, is `active` until withdrawn, and carries no `lastCallAt` or `expiresAt`:
38
- * nothing on the platform namespace keeps those clocks, and listings read them from the calls
39
- * dataset (AM-133). It never carries a provider version id or a secret digest: an in-place secret
40
- * write mints a new version id, which identifies nothing (OPS-2 X1).
41
- */
42
- export type PlatformDeploymentSummaryV1 = Omit<DeploymentSummaryV1, 'record' | 'lastCallAt' | 'expiresAt' | 'state' | 'retirement'> & {
43
- readonly state: 'active' | 'retiring' | 'retired';
44
- readonly retirement?: 'withdrawn';
45
- };
46
- /**
47
- * Admit one platform deployment summary exactly. Any member outside its shape is refused, a
48
- * provider version id or a clock among them; so is a summary whose id, line or URL is not its
49
- * label's, whose owner is not the platform, or whose state and retirement disagree. A reader that
50
- * meets a refused summary lists that label apart, never acting on it.
51
- */
52
- export declare function acceptPlatformDeploymentSummary(input: unknown): PlatformDeploymentSummaryV1;
53
- /**
54
- * The canonical JSON (RFC 8785) of one admitted platform summary, as `summary:<label>` holds it:
55
- * one summary has one text, so a writer that reads it back compares texts.
56
- */
57
- export declare function encodePlatformDeploymentSummary(summary: PlatformDeploymentSummaryV1): string;
@@ -1,117 +1 @@
1
- import { json } from '@astrale-os/kernel-dsl/value';
2
- import { deploymentUrl, parseDeploymentLabel } from './label.js';
3
- /** The owner every summary of a platform namespace names: the platform holds all of its lines. */
4
- export const PLATFORM_OWNER = 'platform';
5
- const REQUIRED = [
6
- 'id',
7
- 'url',
8
- 'label',
9
- 'line',
10
- 'owner',
11
- 'state',
12
- 'retention',
13
- 'createdAt',
14
- 'signingKeyId',
15
- ];
16
- const RETIREMENT = ['retiredAt', 'retirement'];
17
- const STATES = ['active', 'retiring', 'retired'];
18
- const RETENTIONS = ['preview', 'published'];
19
- /** An instant as `Date.prototype.toISOString` writes it, in UTC. */
20
- const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
21
- /** An RFC 7638 JWK thumbprint: the base64url SHA-256 of the public key, 43 characters. */
22
- const KEY_ID = /^[A-Za-z0-9_-]{43}$/u;
23
- /**
24
- * Admit one platform deployment summary exactly. Any member outside its shape is refused, a
25
- * provider version id or a clock among them; so is a summary whose id, line or URL is not its
26
- * label's, whose owner is not the platform, or whose state and retirement disagree. A reader that
27
- * meets a refused summary lists that label apart, never acting on it.
28
- */
29
- export function acceptPlatformDeploymentSummary(input) {
30
- if (input === null || typeof input !== 'object' || Array.isArray(input))
31
- invalid('members');
32
- const value = input;
33
- const keys = Reflect.ownKeys(value);
34
- const retired = RETIREMENT.some((key) => Object.hasOwn(value, key));
35
- const expected = retired ? [...REQUIRED, ...RETIREMENT] : REQUIRED;
36
- if (keys.length !== expected.length || expected.some((key) => !Object.hasOwn(value, key))) {
37
- invalid('members');
38
- }
39
- const label = text(value.label, 'label');
40
- let line;
41
- try {
42
- line = parseDeploymentLabel(label).line;
43
- }
44
- catch {
45
- invalid('label');
46
- }
47
- if (value.id !== label)
48
- invalid('id');
49
- if (value.line !== line)
50
- invalid('line');
51
- const url = text(value.url, 'url');
52
- if (!sameDeploymentUrl(url, label))
53
- invalid('url');
54
- if (value.owner !== PLATFORM_OWNER)
55
- invalid('owner');
56
- const state = value.state;
57
- if (typeof state !== 'string' || !STATES.includes(state))
58
- invalid('state');
59
- if (retired !== (state !== 'active'))
60
- invalid('state');
61
- const retention = value.retention;
62
- if (typeof retention !== 'string' || !RETENTIONS.includes(retention))
63
- invalid('retention');
64
- const createdAt = instant(value.createdAt, 'createdAt');
65
- const signingKeyId = text(value.signingKeyId, 'signingKeyId');
66
- if (!KEY_ID.test(signingKeyId))
67
- invalid('signingKeyId');
68
- if (retired && value.retirement !== 'withdrawn')
69
- invalid('retirement');
70
- return Object.freeze({
71
- id: label,
72
- url,
73
- label,
74
- line,
75
- owner: PLATFORM_OWNER,
76
- state: state,
77
- retention: retention,
78
- createdAt,
79
- signingKeyId,
80
- ...(retired
81
- ? { retiredAt: instant(value.retiredAt, 'retiredAt'), retirement: 'withdrawn' }
82
- : {}),
83
- });
84
- }
85
- /**
86
- * The canonical JSON (RFC 8785) of one admitted platform summary, as `summary:<label>` holds it:
87
- * one summary has one text, so a writer that reads it back compares texts.
88
- */
89
- export function encodePlatformDeploymentSummary(summary) {
90
- return json.serialize(acceptPlatformDeploymentSummary(summary));
91
- }
92
- /** Whether `url` is exactly `https://<label>.<routing domain>` for some routing domain. */
93
- function sameDeploymentUrl(url, label) {
94
- const prefix = `https://${label}.`;
95
- if (!url.startsWith(prefix))
96
- return false;
97
- try {
98
- return deploymentUrl(label, url.slice(prefix.length)) === url;
99
- }
100
- catch {
101
- return false;
102
- }
103
- }
104
- function text(input, member) {
105
- if (typeof input !== 'string' || input.length === 0)
106
- invalid(member);
107
- return input;
108
- }
109
- function instant(input, member) {
110
- const value = text(input, member);
111
- if (!INSTANT.test(value) || new Date(value).toISOString() !== value)
112
- invalid(member);
113
- return value;
114
- }
115
- function invalid(member) {
116
- throw new TypeError(`Platform deployment summary has invalid ${member}.`);
117
- }
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  export { defineAdapter, defineDeployment, isAdapter } from './adapter/index.js';
2
- export type { Adapter, AdapterEnvironmentContext, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, Bundle, BundleContext, Bundler, BundleSource, ConfigureContext, DeployContext, DeploymentCallTarget, DeploymentPhase, DeploymentProgress, DeploymentSecretsState, DeployResult, FailedDeployResult, FailedReleaseDeployResult, LegacyAdapter, LegacyAdapterInput, ListContext, ListedDeployment, MutationKnowledge, Placement, PlacementContext, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, ReadyReleaseDeployResult, ReleaseDeployContext, ReleaseDeploymentPhase, ReleaseDeployResult, RetainContext, } from './adapter/index.js';
2
+ export type { Adapter, AdapterCommand, AdapterCommandContext, AdapterCommandResult, AdapterEnvironmentContext, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, Bundle, BundleContext, Bundler, BundleSource, ConfigureContext, DeployContext, DeploymentCallTarget, DeploymentPhase, DeploymentProgress, DeploymentSecretsState, DeployResult, FailedDeployResult, FailedReleaseDeployResult, LegacyAdapter, LegacyAdapterInput, ListContext, ListedDeployment, MutationKnowledge, Placement, PlacementContext, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, ReadyReleaseDeployResult, ReleaseDeployContext, ReleaseDeploymentPhase, ReleaseDeployResult, RetainContext, } from './adapter/index.js';
3
3
  export { compile, isBuild } from './build/index.js';
4
4
  export type { Build, BuildBundle, BuildDigest, BuildManifestV1, BuildSchema, } from './build/index.js';
5
5
  export { deploy } from './deploy.js';
@@ -86,7 +86,7 @@ function durableContext(input, context, envelope) {
86
86
  const deadline = Date.now() + serving.stepTimeoutMs;
87
87
  const session = input.executor(execution.kernel, execution.id, { deadline, signal });
88
88
  try {
89
- const kernel = keyed(bindSessionOf(session, deadline, signal), await keySequence(execution.id, name));
89
+ const kernel = keyed(bindSessionOf(session, deadline, signal), domain, await keySequence(execution.id, name));
90
90
  const output = await execute(Object.freeze({
91
91
  kernel,
92
92
  query: bindQuery(kernel, domain),
@@ -171,17 +171,29 @@ function bindSessionOf(session, deadline, signal) {
171
171
  return bindSession(session, { deadline, signal });
172
172
  }
173
173
  /**
174
- * Every `invoke` and Mutation of one Step without its own key carries the next key of the Step's
175
- * sequence. The sequence restarts with every attempt, so a retried Step replays the same keys and
176
- * the Kernel answers with the first outcome, or `IDEMPOTENCY_KEY_CONFLICT` if the request changed.
174
+ * Every Mutation and every `invoke` of a durable callable of one Step without its own key carries
175
+ * the next key of the Step's sequence. The sequence restarts with every attempt, so a retried Step
176
+ * replays the same keys: the Kernel answers a Mutation with its receipt and a durable call with the
177
+ * Execution it started, or refuses a request that changed under its key. Any other `invoke` carries
178
+ * no key, because the Kernel accepts one only for a durable Function, and runs again with the Step.
177
179
  */
178
- function keyed(session, next) {
180
+ function keyed(session, domain, next) {
179
181
  const invokeSource = session.invoke;
180
182
  const mutateSource = session.mutate;
181
- const invoke = ((reference, value, options) => invokeSource(reference, value, withKey(options, next)));
183
+ const invoke = ((reference, value, options) => invokeSource(reference, value, declaredDurable(domain, reference) ? withKey(options, next) : options));
182
184
  const mutate = ((author, options) => mutateSource(author, withKey(options, next)));
183
185
  return Object.freeze({ ...session, invoke, mutate });
184
186
  }
187
+ /**
188
+ * Whether the Domain's Schema closure declares the referenced callable durable. An untyped Path
189
+ * names no declaration and a callable outside the closure has none here: both need their own key.
190
+ */
191
+ function declaredDurable(domain, reference) {
192
+ if (reference === null || typeof reference !== 'object')
193
+ return false;
194
+ const key = reference.callable;
195
+ return (typeof key === 'string' && domain.callable(key)?.execution?.durable === true);
196
+ }
185
197
  function withKey(options, next) {
186
198
  if (options?.idempotencyKey !== undefined || options?.body !== undefined)
187
199
  return options;
@@ -0,0 +1,35 @@
1
+ import type { LoadedEnvironment } from './project.js';
2
+ /**
3
+ * `astrale-domain <adapter> <command> <environment> --json` on stdout: what one provider command
4
+ * of the Environment's adapter answered (adapter-cloudflare: `cloudflare plan` and
5
+ * `cloudflare init`, which provision its host on the operator's own account).
6
+ */
7
+ export interface AdapterCommandReportV1 {
8
+ readonly format: 'astrale.adapter-command';
9
+ readonly version: 1;
10
+ /** `<adapter name>@<adapter version>` */
11
+ readonly adapter: string;
12
+ readonly command: string;
13
+ readonly environment: string;
14
+ /** 0 done; 1 refused or blocked. */
15
+ readonly exitCode: 0 | 1;
16
+ /** What the adapter's command answered, as it answered it. */
17
+ readonly result: unknown;
18
+ }
19
+ /**
20
+ * `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: run one
21
+ * provider command an adapter declares, on an Environment that adapter deploys. The CLI only
22
+ * routes: it refuses an Environment whose adapter has another name, a command the adapter does
23
+ * not declare and an option the command does not declare, each before any effect, and prints what
24
+ * the command answers. Exit status: the command's (0 done, 1 refused or blocked), 1 when it could
25
+ * not run.
26
+ */
27
+ export declare function runAdapterCommand(input: {
28
+ readonly adapter: string;
29
+ readonly command: string;
30
+ readonly environment: string;
31
+ readonly definition: LoadedEnvironment;
32
+ readonly options: Readonly<Record<string, string>>;
33
+ readonly json: boolean;
34
+ readonly signal: AbortSignal;
35
+ }): Promise<number>;
@@ -0,0 +1,66 @@
1
+ import { error, info, warn } from './log.js';
2
+ /**
3
+ * `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: run one
4
+ * provider command an adapter declares, on an Environment that adapter deploys. The CLI only
5
+ * routes: it refuses an Environment whose adapter has another name, a command the adapter does
6
+ * not declare and an option the command does not declare, each before any effect, and prints what
7
+ * the command answers. Exit status: the command's (0 done, 1 refused or blocked), 1 when it could
8
+ * not run.
9
+ */
10
+ export async function runAdapterCommand(input) {
11
+ const { adapter, parameters } = input.definition.deployment;
12
+ const name = `${adapter.name}@${adapter.version}`;
13
+ if (adapter.name !== input.adapter) {
14
+ error(`Environment ${input.environment} deploys through adapter ${adapter.name}, not ` +
15
+ `${input.adapter}: run \`astrale-domain ${adapter.name} <command> ${input.environment}\`.`);
16
+ return 1;
17
+ }
18
+ const commands = 'commands' in adapter ? adapter.commands : undefined;
19
+ const command = commands?.[input.command];
20
+ if (command === undefined) {
21
+ const declared = Object.entries(commands ?? {}).map(([candidate, { summary }]) => `${candidate} (${summary})`);
22
+ error(`Adapter ${name} declares no command ${input.command}` +
23
+ (declared.length === 0 ? '.' : `; its commands: ${declared.join('; ')}.`));
24
+ return 1;
25
+ }
26
+ const declaredOptions = command.options ?? {};
27
+ const unknown = Object.keys(input.options).filter((option) => !Object.hasOwn(declaredOptions, option));
28
+ if (unknown.length > 0) {
29
+ const known = Object.entries(declaredOptions).map(([option, description]) => `--${option} (${description})`);
30
+ error(`${input.adapter} ${input.command} takes no option ${unknown.map((option) => `--${option}`).join(', ')}` +
31
+ (known.length === 0 ? '.' : `; its options: ${known.join('; ')}.`));
32
+ return 1;
33
+ }
34
+ let answered;
35
+ try {
36
+ answered = await command.run(parameters, {
37
+ environment: input.environment,
38
+ signal: input.signal,
39
+ options: Object.freeze({ ...input.options }),
40
+ });
41
+ }
42
+ catch (cause) {
43
+ error(`${input.adapter} ${input.command} ${input.environment} (${name}) failed: ` +
44
+ (cause instanceof Error ? cause.message : String(cause)));
45
+ return 1;
46
+ }
47
+ for (const line of answered.warnings ?? [])
48
+ warn(line);
49
+ if (input.json) {
50
+ const report = Object.freeze({
51
+ format: 'astrale.adapter-command',
52
+ version: 1,
53
+ adapter: name,
54
+ command: input.command,
55
+ environment: input.environment,
56
+ exitCode: answered.exitCode,
57
+ result: answered.result,
58
+ });
59
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
60
+ }
61
+ else {
62
+ for (const line of answered.lines)
63
+ info(line);
64
+ }
65
+ return answered.exitCode;
66
+ }
@@ -3,7 +3,8 @@ import { type Version } from '../../platform/versioning/index.js';
3
3
  export declare const COMMANDS: readonly ['build', 'deploy', 'diff', 'list', 'publish', 'test', 'lint', 'package', 'upgrade', 'yank'];
4
4
  export type Command = (typeof COMMANDS)[number];
5
5
  export interface ParsedArgs {
6
- readonly command: Command;
6
+ /** `adapter`: a provider command of an Environment's adapter (`<adapter> <command> <env>`). */
7
+ readonly command: Command | 'adapter';
7
8
  readonly env: string;
8
9
  readonly fix?: boolean;
9
10
  readonly format?: LintReportFormat;
@@ -19,6 +20,11 @@ export interface ParsedArgs {
19
20
  readonly allowDirty?: boolean;
20
21
  /** `list`: instances to ask what they run, in argument order. */
21
22
  readonly instances?: readonly string[];
23
+ /** `<adapter> <command> <env>`: the adapter name and its command. */
24
+ readonly adapter?: string;
25
+ readonly adapterCommand?: string;
26
+ /** `<adapter> <command> <env> --<option> <value>`: the options, by name. */
27
+ readonly options?: Readonly<Record<string, string>>;
22
28
  }
23
29
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
24
30
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
@@ -12,11 +12,18 @@ export const COMMANDS = [
12
12
  'upgrade',
13
13
  'yank',
14
14
  ];
15
+ /** An adapter name or one of its command and option names, as `defineAdapter` admits them. */
16
+ const PROVIDER_NAME = /^[a-z][a-z0-9-]{0,31}$/u;
15
17
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
16
18
  export function parseArgs(argv) {
17
19
  const [command, ...rest] = argv;
18
20
  if (command === 'upgrade')
19
21
  return parseUpgrade(rest);
22
+ if (command !== undefined && !COMMANDS.includes(command)) {
23
+ const provider = parseAdapterCommand(command, rest);
24
+ if (provider !== undefined)
25
+ return provider;
26
+ }
20
27
  const fix = rest.includes('--fix');
21
28
  const deployOnly = rest.includes('--deploy-only');
22
29
  const json = rest.includes('--json');
@@ -84,7 +91,7 @@ export function parseArgs(argv) {
84
91
  command !== 'list' &&
85
92
  command !== 'publish' &&
86
93
  command !== 'yank') {
87
- throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish` or `yank`.');
94
+ throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish`, `yank` or an adapter command.');
88
95
  }
89
96
  if (allowDirty && command !== 'publish') {
90
97
  throw new Error('`--allow-dirty` is only valid for `publish`.');
@@ -222,6 +229,51 @@ function refuseDeployOnly(command, environment) {
222
229
  throw new Error(`${refusal} Run \`astrale-domain ${command} ${environment}\` without it, then install the ` +
223
230
  `URL it prints with \`${installCommand()}\`.`);
224
231
  }
232
+ /**
233
+ * `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: a provider
234
+ * command, routed once the Project is loaded to the adapter of that Environment. Anything that is
235
+ * not exactly an adapter name, a command and an Environment is no command at all (undefined).
236
+ */
237
+ function parseAdapterCommand(adapter, args) {
238
+ if (!PROVIDER_NAME.test(adapter))
239
+ return undefined;
240
+ const positionals = [];
241
+ const options = {};
242
+ let json = false;
243
+ for (let index = 0; index < args.length; index += 1) {
244
+ const argument = args[index];
245
+ if (argument === '--json') {
246
+ json = true;
247
+ continue;
248
+ }
249
+ if (!argument.startsWith('-')) {
250
+ positionals.push(argument);
251
+ continue;
252
+ }
253
+ const equals = argument.indexOf('=');
254
+ const name = argument.slice(2, equals === -1 ? undefined : equals);
255
+ if (!argument.startsWith('--') || !PROVIDER_NAME.test(name)) {
256
+ throw new Error(`Unknown flag "${argument}".`);
257
+ }
258
+ if (Object.hasOwn(options, name))
259
+ throw new Error(`--${name} is given twice.`);
260
+ options[name] =
261
+ equals === -1
262
+ ? requiredValue(`--${name}`, args[++index])
263
+ : requiredValue(`--${name}`, argument.slice(equals + 1));
264
+ }
265
+ const [adapterCommand, env] = positionals;
266
+ if (positionals.length !== 2 || !PROVIDER_NAME.test(adapterCommand))
267
+ return undefined;
268
+ return {
269
+ command: 'adapter',
270
+ env: env,
271
+ adapter,
272
+ adapterCommand: adapterCommand,
273
+ options,
274
+ ...(json ? { json: true } : {}),
275
+ };
276
+ }
225
277
  function parseUpgrade(args) {
226
278
  let channel = 'beta';
227
279
  let check = false;
@@ -30,11 +30,15 @@ Commands:
30
30
  package Prepare public declarations for publication.
31
31
  upgrade Update Astrale dependencies through pnpm (default: beta).
32
32
  yank <version> Take a published version out of resolution, or put it back.
33
+ <adapter> <command> <environment>
34
+ Run a command the Environment's adapter declares for its provider,
35
+ such as cloudflare plan production or cloudflare init production.
33
36
 
34
37
  Options:
35
38
  -h, --help Show help.
36
39
 
37
- Run "astrale-domain <command> --help" for command-specific help.
40
+ Run "astrale-domain <command> --help" for command-specific help. An adapter command that
41
+ does not exist names the commands and options the Environment's adapter declares.
38
42
  `;
39
43
  const COMMAND_HELP = Object.freeze({
40
44
  upgrade: `Update existing Astrale dependencies in a pnpm project or workspace.
@@ -140,19 +144,18 @@ Behavior:
140
144
  The registry and the instances are read through the Astrale CLI (${MINIMUM_INSTALLATIONS_CLI_VERSION} or
141
145
  newer) with the caller's identity, only once a deployment is listed.
142
146
  A registry that cannot be read fails the listing; an unknown instance never does.
143
- A preview expires 30 days after its activation or its last call, whichever is
144
- later; a published deployment never does. Last calls are read from the host as it
145
- counts them, so a host whose last calls cannot be read is not listed. A platform
146
- dispatch namespace reads them from its calls dataset, and without Account Analytics
147
- Read lists its deployments without last calls or expiries.
147
+ Admin's Services and a Cloudflare dispatch namespace keep every deployment until its
148
+ owner retires it, and report no last call or expiry; LAST CALL and EXPIRES show one
149
+ only when an older adapter reports it.
148
150
  Listing changes nothing, at any host, registry or instance.
149
151
  An Environment in legacy direct mode, refused by deploy, or whose adapter lists no
150
152
  deployments is reported on stderr and, with --json, under unlisted. A deployment
151
153
  whose record this SDK does not admit is still listed, from what its host knows.
152
154
  With --json, each deployment carries its installedOn and its call target: the path of
153
155
  its node on Admin's Services, for astrale call "<path>.method.setSecret" --admin, or the
154
- namespace and script of a platform dispatch namespace, which its operator tooling
155
- takes; installations says what of INSTALLED ON is unknown.
156
+ namespace and script of a dispatch namespace, which the hosting kit
157
+ (@astrale-os/adapter-cloudflare/hosting) retires; installations says what of
158
+ INSTALLED ON is unknown.
156
159
  Exit status: 0 listed, 1 a host or the registry could not be read (with --json,
157
160
  nothing on stdout), 2 usage error.
158
161
  `,
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@astrale-os/sdk/cli` — the `astrale-domain` CLI
3
- * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank), its JSON results,
4
- * the deploy mode each command selects for an Environment (CT33), and the explicit dotenv boundary.
3
+ * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank, and the
4
+ * provider commands adapters declare), its JSON results, the deploy mode each command selects for an Environment (CT33), and the explicit
5
+ * dotenv boundary.
5
6
  *
6
7
  * Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
7
8
  * Bun (it imports the project's `astrale.config.ts` directly). This subpath is
@@ -14,6 +15,7 @@ export { run } from './run.js';
14
15
  export type { BuildResultV1 } from './build.js';
15
16
  export type { DeployResultV1, DeploymentSecretsState } from './deploy-result.js';
16
17
  export type { DiffChangeV1, DiffReportV1 } from './diff/index.js';
18
+ export type { AdapterCommandReportV1 } from './adapter-command.js';
17
19
  export type { InstallationSourcesV1, InstallationUnknownReason, InstalledOnV1, InstanceSourceV1, ListedDeploymentV1, ListResultV1, UnlistedEnvironmentV1, } from './list.js';
18
20
  export type { PublishReportV1 } from './publish/index.js';
19
21
  export type { YankReportV1 } from './yank.js';
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@astrale-os/sdk/cli` — the `astrale-domain` CLI
3
- * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank), its JSON results,
4
- * the deploy mode each command selects for an Environment (CT33), and the explicit dotenv boundary.
3
+ * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank, and the
4
+ * provider commands adapters declare), its JSON results, the deploy mode each command selects for an Environment (CT33), and the explicit
5
+ * dotenv boundary.
5
6
  *
6
7
  * Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
7
8
  * Bun (it imports the project's `astrale.config.ts` directly). This subpath is
@@ -26,11 +26,11 @@ export interface ListResultV1 {
26
26
  }
27
27
  /**
28
28
  * One deployment: what its host knows (`DeploymentSummaryV1`), the Environment it was listed for
29
- * and its adapter, its computed name, and its call target. `lastCallAt` and a preview's `expiresAt`
30
- * are as of the listing: a platform namespace (adapter-cloudflare) reads every call from its calls
31
- * dataset, and omits both without Account Analytics Read. Admin's Services (adapter-astrale) keeps
32
- * every deployment until it is retired and counts no call: its rows are `published`, with neither
33
- * clock, and say whether their Service serves them (`current`).
29
+ * and its adapter, its computed name, and its call target. Admin's Services (adapter-astrale) keeps
30
+ * every deployment until it is retired: its rows are `published` and say whether their Service
31
+ * serves them (`current`). A Cloudflare dispatch namespace (adapter-cloudflare) keeps no
32
+ * retention, and knows a deployment's creation instant and key only while its script exists. A
33
+ * last call and an expiry are printed only when an older adapter still reports them.
34
34
  */
35
35
  export type ListedDeploymentV1 = Omit<DeploymentSummaryV1, 'record'> & {
36
36
  /** The Project Environment whose line holds the deployment, as its host listed it. */
@@ -189,13 +189,13 @@ function listedDeployment(hosted, publications, installations) {
189
189
  line: deployment.line,
190
190
  owner: deployment.owner,
191
191
  state: deployment.state,
192
- retention: deployment.retention,
193
- createdAt: deployment.createdAt,
192
+ ...(deployment.retention === undefined ? {} : { retention: deployment.retention }),
193
+ ...(deployment.createdAt === undefined ? {} : { createdAt: deployment.createdAt }),
194
194
  ...(deployment.lastCallAt === undefined ? {} : { lastCallAt: deployment.lastCallAt }),
195
195
  ...(deployment.expiresAt === undefined ? {} : { expiresAt: deployment.expiresAt }),
196
196
  ...(deployment.retiredAt === undefined ? {} : { retiredAt: deployment.retiredAt }),
197
197
  ...(deployment.retirement === undefined ? {} : { retirement: deployment.retirement }),
198
- signingKeyId: deployment.signingKeyId,
198
+ ...(deployment.signingKeyId === undefined ? {} : { signingKeyId: deployment.signingKeyId }),
199
199
  record,
200
200
  ...(recordRefusal === undefined ? {} : { recordRefusal }),
201
201
  ...(deployment.current === undefined ? {} : { current: deployment.current }),
@@ -203,11 +203,11 @@ function listedDeployment(hosted, publications, installations) {
203
203
  });
204
204
  }
205
205
  /**
206
- * Why a deployment its host keeps no record of has none: the adapter lists it without `record`, as a
207
- * platform namespace lists a deployment it has not activated.
206
+ * Why a deployment its host keeps no record of has none: the adapter lists it without `record`, as
207
+ * a dispatch namespace lists a label it retired and never activated.
208
208
  */
209
- const NO_RECORD = 'Its host keeps no record of it: it has not been activated (a deploy still in progress, or one ' +
210
- 'that stopped before activating it).';
209
+ const NO_RECORD = 'Its host keeps no record of it: it was never activated (a deploy still in progress, one that ' +
210
+ 'stopped before activating it, or a label retired before it served).';
211
211
  /** Why an admitted record does not describe the listed deployment, if it does not. */
212
212
  function describesAnother(record, label, listing) {
213
213
  if (record.providerScript !== label) {
@@ -259,18 +259,21 @@ function admitListed(deployment, adapter) {
259
259
  throw new TypeError(`Adapter ${adapter} listed an invalid deployment (${member}).`);
260
260
  };
261
261
  const value = members(deployment) ?? invalid('deployment');
262
- for (const key of ['id', 'url', 'label', 'line', 'owner', 'createdAt', 'signingKeyId']) {
262
+ for (const key of ['id', 'url', 'label', 'line', 'owner']) {
263
263
  if (!text(value[key]))
264
264
  invalid(key);
265
265
  }
266
- for (const key of ['lastCallAt', 'expiresAt', 'retiredAt']) {
266
+ for (const key of ['createdAt', 'signingKeyId', 'lastCallAt', 'expiresAt', 'retiredAt']) {
267
267
  if (value[key] !== undefined && !text(value[key]))
268
268
  invalid(key);
269
269
  }
270
270
  if (!STATES.includes(value.state))
271
271
  invalid('state');
272
- if (value.retention !== 'preview' && value.retention !== 'published')
272
+ if (value.retention !== undefined &&
273
+ value.retention !== 'preview' &&
274
+ value.retention !== 'published') {
273
275
  invalid('retention');
276
+ }
274
277
  if (value.retirement !== undefined &&
275
278
  value.retirement !== 'expired' &&
276
279
  value.retirement !== 'withdrawn') {
@@ -352,10 +355,9 @@ function shortDigest(digest) {
352
355
  return `${digest.slice(0, 'sha256:'.length + 7)}…`;
353
356
  }
354
357
  /**
355
- * When a deployment stops serving: `never` once published, else its preview expiry as its host
356
- * keeps it, 30 days after its activation or its last call, whichever is later (AM-123), with the
357
- * calls counted since the host recorded them; `due` once that instant passed but the host has not
358
- * retired it yet; for a retired deployment, why it was retired.
358
+ * When a deployment stops serving: `never` once published, else the expiry an older adapter
359
+ * reports (`due` once that instant passed), else `—` (a dispatch namespace keeps a deployment
360
+ * until its owner retires it); for a retired deployment, why it was retired.
359
361
  */
360
362
  function expiry(deployment, now) {
361
363
  if (deployment.state === 'retiring')
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { admitRuntime, compile, resolveRuntime } from '../../deployment/index.js';
4
4
  import { packageDomain } from '../packaging/index.js';
5
+ import { runAdapterCommand } from './adapter-command.js';
5
6
  import { parseArgs } from './arguments.js';
6
7
  import { buildResult, bundleBuild, bundlerCopyWarnings, projectBundlers } from './build.js';
7
8
  import { BunRequiredError } from './bun.js';
@@ -64,6 +65,23 @@ export async function run(argv) {
64
65
  const project = await loadProject(configPath);
65
66
  if (parsed.command === 'list')
66
67
  return list(parsed, project, projectDir);
68
+ if (parsed.command === 'adapter') {
69
+ const lifecycle = deploymentLifecycle(`${parsed.adapter} ${parsed.adapterCommand} interrupted.`);
70
+ try {
71
+ return await runAdapterCommand({
72
+ adapter: parsed.adapter,
73
+ command: parsed.adapterCommand,
74
+ environment: parsed.env,
75
+ definition: selectEnvironment(project, parsed.env),
76
+ options: parsed.options ?? {},
77
+ json: parsed.json === true,
78
+ signal: lifecycle.signal,
79
+ });
80
+ }
81
+ finally {
82
+ lifecycle.dispose();
83
+ }
84
+ }
67
85
  if (parsed.command === 'yank')
68
86
  return yank(parsed, project);
69
87
  if (parsed.command === 'publish') {
@@ -63,11 +63,10 @@ export declare const VERIFY_TIMEOUT_MS = 60000;
63
63
  * 6. Ask Admin for the Publication; a version Admin gave another release meanwhile is refused,
64
64
  * naming the winner's release, and this deployment stays a preview. Any other failure says
65
65
  * what CT29 says of it: Admin may have created the Publication (a rerun settles it), Admin made
66
- * none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then, where
67
- * the host has no registry principal to keep it (the platform namespace), `adapter.retain`
68
- * keeps the published deployment (CT36); Admin keeps those of its Services (A7), and a
69
- * Publication this run created whose mark failed warns, without changing the exit code or
70
- * the report.
66
+ * none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then, when
67
+ * the adapter declares `retain` (a host with no registry principal to keep it, CT36), it
68
+ * keeps the published deployment. No first-party adapter declares it: Admin's Services and a
69
+ * Cloudflare dispatch namespace keep every deployment until its owner retires it.
71
70
  *
72
71
  * Steps 0 to 3 make no deploy call. Never installs.
73
72
  */
@@ -44,11 +44,10 @@ const DEFAULT_DEPENDENCIES = Object.freeze({
44
44
  * 6. Ask Admin for the Publication; a version Admin gave another release meanwhile is refused,
45
45
  * naming the winner's release, and this deployment stays a preview. Any other failure says
46
46
  * what CT29 says of it: Admin may have created the Publication (a rerun settles it), Admin made
47
- * none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then, where
48
- * the host has no registry principal to keep it (the platform namespace), `adapter.retain`
49
- * keeps the published deployment (CT36); Admin keeps those of its Services (A7), and a
50
- * Publication this run created whose mark failed warns, without changing the exit code or
51
- * the report.
47
+ * none and a rerun can help, or Admin refused and a rerun meets the same refusal. Then, when
48
+ * the adapter declares `retain` (a host with no registry principal to keep it, CT36), it
49
+ * keeps the published deployment. No first-party adapter declares it: Admin's Services and a
50
+ * Cloudflare dispatch namespace keep every deployment until its owner retires it.
52
51
  *
53
52
  * Steps 0 to 3 make no deploy call. Never installs.
54
53
  */
@@ -126,7 +125,7 @@ export async function runPublish(input, dependencies = DEFAULT_DEPENDENCIES) {
126
125
  if (existing.yanked) {
127
126
  warn(`${name} is yanked; astrale-domain yank ${declared} --undo makes it resolvable again.`);
128
127
  }
129
- // A rerun after the Publication but before its retention completes the retention.
128
+ // A rerun after the Publication but before the adapter's `retain` completes that retention.
130
129
  const kept = await retain(plan, signal);
131
130
  if (kept.state === 'failed')
132
131
  return refuse(...notRetained(name, plan, kept.message));
@@ -195,9 +194,6 @@ export async function runPublish(input, dependencies = DEFAULT_DEPENDENCIES) {
195
194
  return refuse(...notPublished(name, origin, release.url, cause));
196
195
  }
197
196
  progress(`Published: ${name} · ${published.status}`);
198
- if (published.status === 'created' && published.retention === 'failed') {
199
- warn(...notMarked(name, release.url, published.replay));
200
- }
201
197
  const kept = await retain(plan, signal);
202
198
  if (kept.state === 'failed')
203
199
  return refuse(...notRetained(name, plan, kept.message));
@@ -298,11 +294,10 @@ function described(cause) {
298
294
  return cause instanceof RegistryCliError ? `${cause.code}: ${cause.message}` : message(cause);
299
295
  }
300
296
  /**
301
- * Keep the published deployment where its host has no registry principal to mark it: the adapter
302
- * declares `retain` for the platform namespace only (CT36). Admin's Services keeps every
303
- * deployment until it is retired, so adapter-astrale declares none. A `RetainRefusal` (absent or
304
- * retired) is `refused`, which no rerun changes; any other rejection `failed`, which a rerun
305
- * retries.
297
+ * Keep the published deployment where its host has no registry principal to mark it, when the
298
+ * adapter declares `retain` (CT36); an adapter without it keeps every deployment until it is
299
+ * retired, as adapter-astrale and adapter-cloudflare do. A `RetainRefusal` (absent or retired) is
300
+ * `refused`, which no rerun changes; any other rejection `failed`, which a rerun retries.
306
301
  */
307
302
  async function retain(plan, signal) {
308
303
  const { adapter } = plan;
@@ -326,20 +321,6 @@ function notRetained(name, plan, cause) {
326
321
  'Rerun publish: it publishes nothing again and keeps the deployment.',
327
322
  ];
328
323
  }
329
- /**
330
- * Admin created the Publication, then could not mark its Services' deployment retained (A7): the
331
- * version stands, so the run succeeds, but the deployment can still expire as a preview. Admin
332
- * marks on every call that names the version, an `unchanged` one too, so sending the same request
333
- * again marks it; a publish rerun finds the version in the index and never asks Admin.
334
- */
335
- function notMarked(name, url, replay) {
336
- return [
337
- `${name} is published, but its deployment at ${url} was not marked as retained: until it is, it can expire like a preview.`,
338
- 'To mark it, send the same request again: Admin answers unchanged and marks the deployment ' +
339
- 'again (a publish rerun does not send it):',
340
- replay,
341
- ];
342
- }
343
324
  /**
344
325
  * The version names a deployment its host can no longer keep (absent or retired): publishing has
345
326
  * nothing left to do, a rerun cannot keep it, and installing the version fails, never silently.
@@ -39,12 +39,6 @@ export interface RegistryPublishRequest {
39
39
  /** The working tree held changes the commit does not, or no commit could be read. */
40
40
  readonly dirty: boolean;
41
41
  }
42
- /**
43
- * Admin's retention mark of the deployment a Publication names, on this call (CT29
44
- * `PublishRetentionV1`): `marked`, `failed`, or `not-applicable` when Admin's Services do not
45
- * host it. Admin marks on every call that names the version, an `unchanged` one too.
46
- */
47
- export type RegistryPublishRetention = 'marked' | 'failed' | 'not-applicable';
48
42
  /**
49
43
  * What `astrale __domain-registry publish --json` answered (CT29 `PublishResultV1`): `created` by
50
44
  * this call, or `unchanged` when the version already named this release.
@@ -54,13 +48,6 @@ export interface RegistryPublishResult {
54
48
  readonly publication: RegistryPublication & {
55
49
  readonly url: string;
56
50
  };
57
- /** Absent when the CLI answered no retention mark this SDK knows. */
58
- readonly retention?: RegistryPublishRetention;
59
- /**
60
- * The command a person runs by hand to send this same request again, its document on stdin:
61
- * the one a publish that may have applied names.
62
- */
63
- readonly replay: string;
64
51
  }
65
52
  /** Every Publication of one Domain the caller may read, as `astrale domain versions` lists it. */
66
53
  export interface RegistryIndex {
@@ -127,7 +127,7 @@ export async function openDomainRegistry(input, dependencies = {
127
127
  stdout: DOCUMENT_LIMIT,
128
128
  timeoutMs: PUBLISH_TIMEOUT_MS,
129
129
  change: true,
130
- }), request, commandLine(invocation));
130
+ }), request);
131
131
  },
132
132
  async yank(origin, version, options) {
133
133
  const reference = `${origin}@${version}`;
@@ -256,10 +256,10 @@ function publication(input, origin) {
256
256
  /**
257
257
  * Admit the CT29 `PublishResultV1` only as the answer to this request: the same version, URL and
258
258
  * release digest, so a CLI that answered for another Publication is never reported as published.
259
- * Its retention mark never decides that: one this SDK does not know is left out, so the
260
- * Publication still reads as published.
259
+ * Any other key is ignored, such as the `retention` mark an Admin before admin#455 still answers:
260
+ * Admin keeps no Services mark any more, since Services keeps every deployment until it is retired.
261
261
  */
262
- function decodePublishResult(document, request, replay) {
262
+ function decodePublishResult(document, request) {
263
263
  const named = document.publication;
264
264
  if (document.format !== 'astrale.registry-publish-result' ||
265
265
  document.version !== 1 ||
@@ -277,13 +277,8 @@ function decodePublishResult(document, request, replay) {
277
277
  return Object.freeze({
278
278
  status: document.status,
279
279
  publication: Object.freeze({ ...published, url: named.url }),
280
- ...(isRetention(document.retention) ? { retention: document.retention } : {}),
281
- replay,
282
280
  });
283
281
  }
284
- function isRetention(input) {
285
- return input === 'marked' || input === 'failed' || input === 'not-applicable';
286
- }
287
282
  function decodeBundleDocument(document, origin, expected) {
288
283
  const named = document.publication;
289
284
  if (document.format !== 'astrale.registry-bundle' ||
@@ -2,7 +2,7 @@
2
2
  * The Admin Domain registry as Project commands read and change it: through the Astrale CLI's
3
3
  * registry plumbing (CT29), with the caller's own credential and the CLI's configured Admin target.
4
4
  */
5
- export { RegistryCliError, openDomainRegistry, type DomainRegistry, type DomainRegistryDependencies, type RegistryBundle, type RegistryDigest, type RegistryIndex, type RegistryPublication, type RegistryPublishRequest, type RegistryPublishResult, type RegistryPublishRetention, type RegistryYank, } from './client.js';
5
+ export { RegistryCliError, openDomainRegistry, type DomainRegistry, type DomainRegistryDependencies, type RegistryBundle, type RegistryDigest, type RegistryIndex, type RegistryPublication, type RegistryPublishRequest, type RegistryPublishResult, type RegistryYank, } from './client.js';
6
6
  export { MINIMUM_INSTALLATIONS_CLI_VERSION, MINIMUM_REGISTRY_CLI_VERSION, resolveRegistryExecutable, } from './executable.js';
7
7
  export { isAdminFailure } from './installations.js';
8
8
  export type { InstallationUnknownReason, InstanceInstallation, InstanceListing, } from './installations.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.6.0-beta.20",
3
+ "version": "0.6.0-beta.22",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",