@astrale-os/adapter-astrale 0.4.1 → 0.5.0-beta.2
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 +11 -0
- package/dist/astrale.d.ts +73 -5
- package/dist/astrale.d.ts.map +1 -1
- package/dist/astrale.js +227 -113
- package/dist/astrale.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/managed-service.d.ts +6 -13
- package/dist/managed-service.d.ts.map +1 -1
- package/dist/managed-service.js +26 -33
- package/dist/managed-service.js.map +1 -1
- package/dist/params.d.ts +10 -11
- package/dist/params.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/astrale.ts +304 -139
- package/src/index.ts +4 -4
- package/src/managed-service.ts +37 -52
- package/src/params.ts +10 -12
- package/template/astrale.config.ts +7 -7
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# `@astrale-os/adapter-astrale`
|
|
2
|
+
|
|
3
|
+
Managed Astrale platform deployment adapter for Astrale Domains.
|
|
4
|
+
|
|
5
|
+
During the SDK beta program, install the beta cohort explicitly:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @astrale-os/sdk@beta @astrale-os/adapter-astrale@beta
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Bare installs return to stable releases when the cohort is promoted and npm `latest` advances.
|
package/dist/astrale.d.ts
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ships the domain THROUGH the target instance's `services` domain instead of to
|
|
5
5
|
* the author's own cloud account. `deploy` builds the same single-module workerd
|
|
6
|
-
* bundle the Cloudflare adapter ships (shared codegen +
|
|
7
|
-
* --dry-run`), then:
|
|
6
|
+
* bundle and frontend artifact the Cloudflare adapter ships (shared codegen +
|
|
7
|
+
* `wrangler deploy --dry-run`), then:
|
|
8
8
|
*
|
|
9
9
|
* 1. hosts it on the instance's services domain — `CloudflareWorker.deploy
|
|
10
10
|
* { name, entry, modules, assets, vars }` → a public worker `url`;
|
|
11
11
|
* 2. (optional) pushes runtime secrets via the service's `setSecret`;
|
|
12
12
|
* 3. installs the domain on the instance from that `url`
|
|
13
13
|
* (`astrale domain install <url>` → the child kernel's `Domain.install`,
|
|
14
|
-
* which fetches
|
|
14
|
+
* which fetches, verifies, and reconciles the signed Publication).
|
|
15
15
|
*
|
|
16
16
|
* The admin control plane is NOT involved: hosting moved out of admin into the
|
|
17
17
|
* per-instance `services` domain (a separate bounded context). No Cloudflare
|
|
@@ -24,9 +24,60 @@
|
|
|
24
24
|
* `watch` is identical to the Cloudflare adapter's local dev (wrangler dev on
|
|
25
25
|
* localhost) — managed deploys change WHERE the worker runs, not how you iterate.
|
|
26
26
|
*/
|
|
27
|
-
import type { DomainAdapter } from '@astrale-os/sdk';
|
|
28
|
-
import
|
|
27
|
+
import type { CompiledFrontendArtifact, DomainAdapter } from '@astrale-os/sdk';
|
|
28
|
+
import { buildCloudflareWorker, buildFrontend, logTo, prepare, runWranglerBundle, runWranglerDev, runWranglerTypes, watchFrontend } from '@astrale-os/adapter-cloudflare/build';
|
|
29
|
+
import { loadDeclaredSecrets } from '@astrale-os/sdk/cli';
|
|
30
|
+
import type { AstraleParams } from './params.js';
|
|
31
|
+
import { ensureServicesDomainInstalled, waitForManagedServiceReady } from './managed-service.js';
|
|
29
32
|
export declare function astrale(envs: Record<string, AstraleParams>): DomainAdapter<AstraleParams>;
|
|
33
|
+
export interface AstraleAdapterDependencies {
|
|
34
|
+
readonly astraleBin: typeof astraleBin;
|
|
35
|
+
readonly astraleCall: typeof astraleCall;
|
|
36
|
+
readonly buildCloudflareWorker: typeof buildCloudflareWorker;
|
|
37
|
+
readonly buildFrontend: typeof buildFrontend;
|
|
38
|
+
readonly ensureServicesDomainInstalled: typeof ensureServicesDomainInstalled;
|
|
39
|
+
readonly loadDeclaredSecrets: typeof loadDeclaredSecrets;
|
|
40
|
+
readonly logTo: typeof logTo;
|
|
41
|
+
readonly prepare: typeof prepare;
|
|
42
|
+
readonly readBytes: (path: string) => Buffer;
|
|
43
|
+
readonly readText: (path: string) => string;
|
|
44
|
+
readonly runAstrale: typeof runAstrale;
|
|
45
|
+
readonly runWranglerBundle: typeof runWranglerBundle;
|
|
46
|
+
readonly runWranglerDev: typeof runWranglerDev;
|
|
47
|
+
readonly runWranglerTypes: typeof runWranglerTypes;
|
|
48
|
+
readonly watchFrontend: typeof watchFrontend;
|
|
49
|
+
readonly waitForManagedServiceReady: typeof waitForManagedServiceReady;
|
|
50
|
+
}
|
|
51
|
+
/** Exact orchestration seam used by adapter-level managed deployment qualification. */
|
|
52
|
+
export declare function astraleWithDependencies(envs: Record<string, AstraleParams>, dependencies: AstraleAdapterDependencies): DomainAdapter<AstraleParams>;
|
|
53
|
+
export interface ManagedDeploymentVersionInput {
|
|
54
|
+
readonly bundle: Uint8Array;
|
|
55
|
+
readonly frontend?: CompiledFrontendArtifact;
|
|
56
|
+
readonly compatibilityDate?: string;
|
|
57
|
+
readonly compatibilityFlags?: readonly string[];
|
|
58
|
+
readonly serviceBindings?: readonly {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly service: string;
|
|
61
|
+
}[];
|
|
62
|
+
readonly vars?: Readonly<Record<string, string>>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Stable generation for the whole managed serving contract. The Worker bytes
|
|
66
|
+
* alone are insufficient: frontend files/routes, compatibility settings,
|
|
67
|
+
* bindings, and vars can all change without producing a different bundle.
|
|
68
|
+
*/
|
|
69
|
+
export declare function managedDeploymentVersion(input: ManagedDeploymentVersionInput): string;
|
|
70
|
+
/** Preserve author vars while reserving the generation value owned by the adapter. */
|
|
71
|
+
export declare function managedDeploymentVars(vars: Readonly<Record<string, string>> | undefined, deploymentVersion: string): Readonly<Record<string, string>>;
|
|
72
|
+
type AssetFile = {
|
|
73
|
+
path: string;
|
|
74
|
+
contentBase64: string;
|
|
75
|
+
contentType?: string;
|
|
76
|
+
};
|
|
77
|
+
/** Convert the materialized frontend into the services provider's immutable
|
|
78
|
+
* asset envelope. External frontends deliberately retain routes but no local
|
|
79
|
+
* files, so the callable worker and browser host remain independently swappable. */
|
|
80
|
+
export declare function managedFrontendAssets(frontend: CompiledFrontendArtifact | undefined): readonly AssetFile[];
|
|
30
81
|
/** Semantic method address; independent of the installed domain's tree placement. */
|
|
31
82
|
export declare function servicesDeployMethodFor(origin: string): string;
|
|
32
83
|
/** Service name from the origin: first DNS label (e.g. `my-notes.example.dev` → `my-notes`). */
|
|
@@ -36,4 +87,21 @@ export declare function serviceNameFor(origin: string): string;
|
|
|
36
87
|
* (serviceNameFor) can collide across domains (`app.foo.dev` / `app.bar.dev`),
|
|
37
88
|
* so the service NODE is placed by this; the display name stays the short label. */
|
|
38
89
|
export declare function serviceSlugFor(origin: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Invoke the user's `astrale` CLI for one platform call (against the target
|
|
92
|
+
* INSTANCE) and parse its JSON output. The CLI owns identity (IdP session,
|
|
93
|
+
* audience scoping, delegation minting) — reimplementing that here would fork
|
|
94
|
+
* the trust path.
|
|
95
|
+
*/
|
|
96
|
+
declare function astraleCall(projectDir: string, params: AstraleParams, call: {
|
|
97
|
+
instance: string;
|
|
98
|
+
path: string;
|
|
99
|
+
data: unknown;
|
|
100
|
+
viaStdin?: boolean;
|
|
101
|
+
}): Promise<unknown>;
|
|
102
|
+
/** Spawn the astrale CLI, feed optional stdin, and return stdout. Throws with bounded diagnostics. */
|
|
103
|
+
declare function runAstrale(bin: string, cwd: string, args: string[], stdin?: string): Promise<string>;
|
|
104
|
+
/** The user's astrale CLI: ~/.astrale/bin/astrale when present, else PATH. */
|
|
105
|
+
declare function astraleBin(_projectDir: string): string;
|
|
106
|
+
export {};
|
|
39
107
|
//# sourceMappingURL=astrale.d.ts.map
|
package/dist/astrale.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"astrale.d.ts","sourceRoot":"","sources":["../src/astrale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"astrale.d.ts","sourceRoot":"","sources":["../src/astrale.ts"],"names":[],"mappings":"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"}
|
package/dist/astrale.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ships the domain THROUGH the target instance's `services` domain instead of to
|
|
5
5
|
* the author's own cloud account. `deploy` builds the same single-module workerd
|
|
6
|
-
* bundle the Cloudflare adapter ships (shared codegen +
|
|
7
|
-
* --dry-run`), then:
|
|
6
|
+
* bundle and frontend artifact the Cloudflare adapter ships (shared codegen +
|
|
7
|
+
* `wrangler deploy --dry-run`), then:
|
|
8
8
|
*
|
|
9
9
|
* 1. hosts it on the instance's services domain — `CloudflareWorker.deploy
|
|
10
10
|
* { name, entry, modules, assets, vars }` → a public worker `url`;
|
|
11
11
|
* 2. (optional) pushes runtime secrets via the service's `setSecret`;
|
|
12
12
|
* 3. installs the domain on the instance from that `url`
|
|
13
13
|
* (`astrale domain install <url>` → the child kernel's `Domain.install`,
|
|
14
|
-
* which fetches
|
|
14
|
+
* which fetches, verifies, and reconciles the signed Publication).
|
|
15
15
|
*
|
|
16
16
|
* The admin control plane is NOT involved: hosting moved out of admin into the
|
|
17
17
|
* per-instance `services` domain (a separate bounded context). No Cloudflare
|
|
@@ -27,31 +27,64 @@
|
|
|
27
27
|
// The shared Cloudflare-Workers build toolkit — the managed adapter ships the
|
|
28
28
|
// SAME bundle the direct `cloudflare` adapter does, then routes it through the
|
|
29
29
|
// platform instead of `wrangler deploy`.
|
|
30
|
-
import {
|
|
30
|
+
import { buildCloudflareWorker, buildFrontend, logTo, prepare, runWranglerBundle, runWranglerDev, runWranglerTypes, watchFrontend, } from '@astrale-os/adapter-cloudflare/build';
|
|
31
31
|
import { defineAdapter } from '@astrale-os/sdk';
|
|
32
32
|
import { loadDeclaredSecrets } from '@astrale-os/sdk/cli';
|
|
33
33
|
import { spawn } from 'node:child_process';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
35
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { ensureServicesDomainInstalled, waitForManagedServiceReady } from './managed-service.js';
|
|
37
38
|
const DEFAULT_PORT = 8787;
|
|
38
39
|
const DEFAULT_SERVICES_ORIGIN = 'services.astrale.ai';
|
|
40
|
+
const DEPLOYMENT_VERSION_BINDING = 'ASTRALE_DEPLOYMENT_VERSION';
|
|
39
41
|
export function astrale(envs) {
|
|
42
|
+
return astraleWithDependencies(envs, {
|
|
43
|
+
astraleBin,
|
|
44
|
+
astraleCall,
|
|
45
|
+
buildCloudflareWorker,
|
|
46
|
+
buildFrontend,
|
|
47
|
+
ensureServicesDomainInstalled,
|
|
48
|
+
loadDeclaredSecrets,
|
|
49
|
+
logTo,
|
|
50
|
+
prepare,
|
|
51
|
+
readBytes: (path) => readFileSync(path),
|
|
52
|
+
readText: (path) => readFileSync(path, 'utf8'),
|
|
53
|
+
runAstrale,
|
|
54
|
+
runWranglerBundle,
|
|
55
|
+
runWranglerDev,
|
|
56
|
+
runWranglerTypes,
|
|
57
|
+
watchFrontend,
|
|
58
|
+
waitForManagedServiceReady,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Exact orchestration seam used by adapter-level managed deployment qualification. */
|
|
62
|
+
export function astraleWithDependencies(envs, dependencies) {
|
|
40
63
|
return defineAdapter({
|
|
41
64
|
name: 'astrale',
|
|
42
65
|
envs,
|
|
66
|
+
async build(params, ctx) {
|
|
67
|
+
await dependencies.buildCloudflareWorker({
|
|
68
|
+
params,
|
|
69
|
+
context: ctx,
|
|
70
|
+
onLog: dependencies.logTo(),
|
|
71
|
+
});
|
|
72
|
+
},
|
|
43
73
|
// Local dev is unchanged: wrangler dev on localhost. Managed deploys change
|
|
44
74
|
// WHERE the worker ships, not the inner loop.
|
|
45
75
|
async watch(params, ctx) {
|
|
46
|
-
const
|
|
47
|
-
|
|
76
|
+
const frontend = await dependencies.buildFrontend({
|
|
77
|
+
definition: ctx.definition,
|
|
78
|
+
projectDir: ctx.projectDir,
|
|
79
|
+
onLog: dependencies.logTo(),
|
|
80
|
+
});
|
|
81
|
+
const { configPath } = await dependencies.prepare({
|
|
48
82
|
...(params.vars ? { vars: params.vars } : {}),
|
|
49
83
|
...(params.host ? { host: params.host } : {}),
|
|
50
84
|
port: params.port ?? DEFAULT_PORT,
|
|
51
|
-
}, ctx, 'dev',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const handle = await runWranglerDev({
|
|
85
|
+
}, ctx, 'dev', frontend);
|
|
86
|
+
const onLog = dependencies.logTo();
|
|
87
|
+
const handle = await dependencies.runWranglerDev({
|
|
55
88
|
projectDir: ctx.projectDir,
|
|
56
89
|
configPath,
|
|
57
90
|
port: params.port ?? DEFAULT_PORT,
|
|
@@ -59,61 +92,104 @@ export function astrale(envs) {
|
|
|
59
92
|
autoPickPort: params.port === undefined,
|
|
60
93
|
...(params.host ? { ip: '0.0.0.0' } : {}),
|
|
61
94
|
onReload: ctx.onReload,
|
|
62
|
-
onLog
|
|
95
|
+
onLog,
|
|
63
96
|
});
|
|
64
|
-
|
|
97
|
+
let frontendWatch;
|
|
98
|
+
try {
|
|
99
|
+
frontendWatch = dependencies.watchFrontend({
|
|
100
|
+
definition: ctx.definition,
|
|
101
|
+
projectDir: ctx.projectDir,
|
|
102
|
+
onLog,
|
|
103
|
+
onRebuild: ctx.onReload,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
try {
|
|
108
|
+
await handle.stop();
|
|
109
|
+
}
|
|
110
|
+
catch (stopError) {
|
|
111
|
+
throw new AggregateError([error, stopError], 'frontend watcher failed to start and the local worker failed to stop');
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
url: params.host ?? handle.url,
|
|
117
|
+
async stop() {
|
|
118
|
+
const results = await Promise.allSettled([
|
|
119
|
+
frontendWatch?.stop() ?? Promise.resolve(),
|
|
120
|
+
handle.stop(),
|
|
121
|
+
]);
|
|
122
|
+
const failures = results
|
|
123
|
+
.filter((result) => result.status === 'rejected')
|
|
124
|
+
.map((result) => result.reason);
|
|
125
|
+
if (failures.length === 1)
|
|
126
|
+
throw failures[0];
|
|
127
|
+
if (failures.length > 1)
|
|
128
|
+
throw new AggregateError(failures, 'local development failed to stop');
|
|
129
|
+
},
|
|
130
|
+
};
|
|
65
131
|
},
|
|
66
132
|
// Config hot-regen for the local dev loop: same codegen as `watch` (the
|
|
67
133
|
// running `wrangler dev` reloads its generated config + entry itself) —
|
|
68
134
|
// the params mapping must stay identical to `watch`'s prepare call.
|
|
69
135
|
async regenerate(params, ctx) {
|
|
70
|
-
const
|
|
71
|
-
|
|
136
|
+
const frontend = await dependencies.buildFrontend({
|
|
137
|
+
definition: ctx.definition,
|
|
138
|
+
projectDir: ctx.projectDir,
|
|
139
|
+
onLog: dependencies.logTo(),
|
|
140
|
+
});
|
|
141
|
+
const { configPath } = await dependencies.prepare({
|
|
72
142
|
...(params.vars ? { vars: params.vars } : {}),
|
|
73
143
|
...(params.host ? { host: params.host } : {}),
|
|
74
144
|
port: params.port ?? DEFAULT_PORT,
|
|
75
|
-
}, ctx, 'dev',
|
|
76
|
-
|
|
77
|
-
|
|
145
|
+
}, ctx, 'dev', frontend);
|
|
146
|
+
await dependencies.runWranglerTypes({
|
|
147
|
+
projectDir: ctx.projectDir,
|
|
148
|
+
configPath,
|
|
149
|
+
onLog: dependencies.logTo(),
|
|
150
|
+
});
|
|
78
151
|
},
|
|
79
152
|
async deploy(params, ctx) {
|
|
80
|
-
const log = logTo();
|
|
153
|
+
const log = dependencies.logTo();
|
|
81
154
|
const instance = params.instance;
|
|
82
155
|
if (!instance) {
|
|
83
156
|
throw new Error(`astrale adapter: this env has no "instance" — managed deploys need the target ` +
|
|
84
157
|
`instance slug (e.g. prod: { instance: '<your-instance-slug>' }).`);
|
|
85
158
|
}
|
|
86
159
|
const servicesOrigin = params.servicesOrigin ?? DEFAULT_SERVICES_ORIGIN;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
160
|
+
const origin = ctx.definition.schema.origin;
|
|
161
|
+
const schemaRevision = ctx.definition.domain.$.revision;
|
|
162
|
+
const name = params.name ?? serviceNameFor(origin);
|
|
163
|
+
const runProjectAstrale = (args) => dependencies.runAstrale(dependencies.astraleBin(ctx.projectDir), ctx.projectDir, args);
|
|
164
|
+
await dependencies.ensureServicesDomainInstalled({
|
|
91
165
|
runAstrale: runProjectAstrale,
|
|
92
166
|
instance,
|
|
93
167
|
servicesOrigin,
|
|
94
168
|
identityArgs: identityArgs(params),
|
|
95
169
|
log,
|
|
96
170
|
});
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
171
|
+
// Materialize the exact Domain-owned frontend recipe through the same
|
|
172
|
+
// build boundary used by direct Cloudflare deployments. The managed
|
|
173
|
+
// provider receives those immutable files separately from the Worker
|
|
174
|
+
// module while the Worker publication retains the same route metadata.
|
|
175
|
+
const frontend = await dependencies.buildFrontend({
|
|
176
|
+
definition: ctx.definition,
|
|
177
|
+
projectDir: ctx.projectDir,
|
|
178
|
+
onLog: log,
|
|
179
|
+
});
|
|
180
|
+
const assetFiles = managedFrontendAssets(frontend);
|
|
181
|
+
if (assetFiles.length) {
|
|
182
|
+
log(`packed frontend (${assetFiles.length} files) — declared routes ship managed`);
|
|
104
183
|
}
|
|
105
184
|
// 1. Codegen + bundle: the exact worker module a Cloudflare deploy would
|
|
106
185
|
// ship, produced locally with `wrangler deploy --dry-run` (no auth).
|
|
107
|
-
const { configPath } = await prepare({}, ctx, 'dev',
|
|
108
|
-
servesClient: Boolean(clientDir),
|
|
109
|
-
bundleAssets: false,
|
|
110
|
-
});
|
|
186
|
+
const { configPath } = await dependencies.prepare({}, ctx, 'dev', frontend);
|
|
111
187
|
// The generated wrangler config is the single source of truth for the
|
|
112
188
|
// runtime contract the bundle is validated against (compat date + flags,
|
|
113
189
|
// incl. any `params.wrangler` overrides). Carry it to the managed deploy so
|
|
114
190
|
// the live script matches the local dry-run exactly — notably `nodejs_compat`
|
|
115
191
|
// for the `node:*` imports the worker runtime pulls in.
|
|
116
|
-
const wranglerConfig = JSON.parse(
|
|
192
|
+
const wranglerConfig = JSON.parse(dependencies.readText(configPath));
|
|
117
193
|
// Carry the worker's outbound service bindings (e.g. ROUTER → admin-router)
|
|
118
194
|
// to the managed deploy — without them the deployed worker can't ride the
|
|
119
195
|
// router to reach the kernel and its first credential verification 522s on
|
|
@@ -123,19 +199,28 @@ export function astrale(envs) {
|
|
|
123
199
|
.filter((s) => s.binding !== 'SELF')
|
|
124
200
|
.map((s) => ({ name: s.binding, service: s.service }));
|
|
125
201
|
const outDir = join(ctx.projectDir, '.astrale', 'dist-managed');
|
|
126
|
-
const bundlePath = await runWranglerBundle({
|
|
202
|
+
const bundlePath = await dependencies.runWranglerBundle({
|
|
127
203
|
projectDir: ctx.projectDir,
|
|
128
204
|
configPath,
|
|
129
205
|
outDir,
|
|
130
206
|
onLog: log,
|
|
131
207
|
});
|
|
132
|
-
const bundle =
|
|
208
|
+
const bundle = dependencies.readBytes(bundlePath);
|
|
209
|
+
const deploymentVersion = managedDeploymentVersion({
|
|
210
|
+
bundle,
|
|
211
|
+
frontend,
|
|
212
|
+
compatibilityDate: wranglerConfig.compatibility_date,
|
|
213
|
+
compatibilityFlags: wranglerConfig.compatibility_flags,
|
|
214
|
+
serviceBindings,
|
|
215
|
+
vars: params.vars,
|
|
216
|
+
});
|
|
217
|
+
const deploymentVars = managedDeploymentVars(params.vars, deploymentVersion);
|
|
133
218
|
// 2. Host it on the instance's services domain. `CloudflareWorker.deploy`
|
|
134
219
|
// upserts a worker by name on the platform's CF Workers-for-Platforms
|
|
135
220
|
// backend and returns its public URL. Payload rides STDIN — a bundle is
|
|
136
221
|
// megabytes of base64, far past argv limits.
|
|
137
222
|
log(`deploying "${name}" to the services domain on "${instance}"…`);
|
|
138
|
-
const deployed = (await astraleCall(ctx.projectDir, params, {
|
|
223
|
+
const deployed = (await dependencies.astraleCall(ctx.projectDir, params, {
|
|
139
224
|
instance,
|
|
140
225
|
path: servicesDeployMethodFor(servicesOrigin),
|
|
141
226
|
data: {
|
|
@@ -144,7 +229,7 @@ export function astrale(envs) {
|
|
|
144
229
|
// the services domain provisions no `/services` folder — so the service
|
|
145
230
|
// is a root child keyed by the origin slug (unique per instance); `name`
|
|
146
231
|
// stays the short display label.
|
|
147
|
-
path: `/${serviceSlugFor(
|
|
232
|
+
path: `/${serviceSlugFor(origin)}`,
|
|
148
233
|
name,
|
|
149
234
|
entry: 'index.mjs',
|
|
150
235
|
...(wranglerConfig.compatibility_date
|
|
@@ -159,12 +244,12 @@ export function astrale(envs) {
|
|
|
159
244
|
? {
|
|
160
245
|
assets: {
|
|
161
246
|
files: assetFiles,
|
|
162
|
-
notFound: '
|
|
247
|
+
notFound: 'none',
|
|
163
248
|
runWorkerFirst: true,
|
|
164
249
|
},
|
|
165
250
|
}
|
|
166
251
|
: {}),
|
|
167
|
-
|
|
252
|
+
vars: deploymentVars,
|
|
168
253
|
},
|
|
169
254
|
viaStdin: true,
|
|
170
255
|
}));
|
|
@@ -176,18 +261,18 @@ export function astrale(envs) {
|
|
|
176
261
|
// 3. Author runtime secrets: read the env's dotenv locally and push each
|
|
177
262
|
// via the service's write-only `setSecret` (values ride stdin, never argv).
|
|
178
263
|
if (params.secrets) {
|
|
179
|
-
const secrets = loadDeclaredSecrets(join(ctx.projectDir, params.secrets), params.secrets);
|
|
264
|
+
const secrets = dependencies.loadDeclaredSecrets(join(ctx.projectDir, params.secrets), params.secrets);
|
|
180
265
|
const keys = Object.keys(secrets);
|
|
181
266
|
if (keys.length)
|
|
182
267
|
log(`setting ${keys.length} secret(s) from ${params.secrets}…`);
|
|
183
268
|
for (const [key, value] of Object.entries(secrets)) {
|
|
184
|
-
await astraleCall(ctx.projectDir, params, {
|
|
269
|
+
await dependencies.astraleCall(ctx.projectDir, params, {
|
|
185
270
|
instance,
|
|
186
271
|
// setSecret is an instance method on the deployed service node, which
|
|
187
272
|
// step 2 placed at `/${serviceSlugFor(origin)}` (a root child — the
|
|
188
273
|
// services domain provisions no `/services` folder). Address it there,
|
|
189
274
|
// NOT under a `/services/<name>` path that doesn't exist.
|
|
190
|
-
path: `/${serviceSlugFor(
|
|
275
|
+
path: `/${serviceSlugFor(origin)}::setSecret`,
|
|
191
276
|
data: { name: key, value },
|
|
192
277
|
viaStdin: true,
|
|
193
278
|
});
|
|
@@ -196,17 +281,23 @@ export function astrale(envs) {
|
|
|
196
281
|
// Cloudflare's dispatch namespace can acknowledge a script before the
|
|
197
282
|
// public tenant host is consistently serving the new worker. Installing
|
|
198
283
|
// before `/meta` + JWKS are reachable turns that propagation window into a
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
284
|
+
// Publication fetch abort with an outcome-ambiguous install request.
|
|
285
|
+
log(`waiting for managed service "${origin}" at ${url}…`);
|
|
286
|
+
await dependencies.waitForManagedServiceReady(url, origin, {
|
|
287
|
+
log,
|
|
288
|
+
expectedSchemaRevision: schemaRevision,
|
|
289
|
+
expectedDeploymentVersion: deploymentVersion,
|
|
290
|
+
});
|
|
203
291
|
// 4. Install the domain on the instance from the hosted URL. The child
|
|
204
|
-
// kernel fetches the signed
|
|
292
|
+
// kernel fetches and verifies the signed Publication and bundle.
|
|
205
293
|
// A managed deploy ALWAYS aliases identity — the domain's origin
|
|
206
|
-
// (`
|
|
294
|
+
// (`origin`) is served from the platform host (`…svc.<routing>`)
|
|
207
295
|
// it never matches — so consent to the override is implicit and required.
|
|
208
|
-
log(`installing "${
|
|
209
|
-
|
|
296
|
+
log(`installing "${origin}" on "${instance}" from ${url}…`);
|
|
297
|
+
// A timeout, reset, or 5xx after delivery is outcome-ambiguous: the first
|
|
298
|
+
// install may already have committed. Never replay without a public
|
|
299
|
+
// idempotency/reconciliation contract.
|
|
300
|
+
await dependencies.runAstrale(dependencies.astraleBin(ctx.projectDir), ctx.projectDir, [
|
|
210
301
|
'domain',
|
|
211
302
|
'install',
|
|
212
303
|
url,
|
|
@@ -218,14 +309,11 @@ export function astrale(envs) {
|
|
|
218
309
|
'--timeout',
|
|
219
310
|
'240000',
|
|
220
311
|
...identityArgs(params),
|
|
221
|
-
])
|
|
222
|
-
log,
|
|
223
|
-
beforeRetry: () => waitForManagedServiceReady(url, ctx.domain.origin, { log, expectedSchemaHash }),
|
|
224
|
-
});
|
|
312
|
+
]);
|
|
225
313
|
return {
|
|
226
314
|
url,
|
|
227
315
|
nextSteps: ` installed on "${instance}" — call it:\n` +
|
|
228
|
-
` astrale call "/${
|
|
316
|
+
` astrale call "/${origin}/..." -i ${instance}`,
|
|
229
317
|
};
|
|
230
318
|
},
|
|
231
319
|
secretsFile(params) {
|
|
@@ -233,29 +321,61 @@ export function astrale(envs) {
|
|
|
233
321
|
},
|
|
234
322
|
});
|
|
235
323
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Stable generation for the whole managed serving contract. The Worker bytes
|
|
326
|
+
* alone are insufficient: frontend files/routes, compatibility settings,
|
|
327
|
+
* bindings, and vars can all change without producing a different bundle.
|
|
328
|
+
*/
|
|
329
|
+
export function managedDeploymentVersion(input) {
|
|
330
|
+
const hash = createHash('sha256');
|
|
331
|
+
const assets = managedFrontendAssets(input.frontend);
|
|
332
|
+
updateHashFrame(hash, 'format', 'astrale-managed-deployment-v2');
|
|
333
|
+
updateHashFrame(hash, 'worker', input.bundle);
|
|
334
|
+
updateHashFrame(hash, 'serving', JSON.stringify({
|
|
335
|
+
frontend: input.frontend === undefined
|
|
336
|
+
? null
|
|
337
|
+
: {
|
|
338
|
+
source: input.frontend.artifact.source.kind,
|
|
339
|
+
digest: input.frontend.digest,
|
|
340
|
+
assets: assets.length > 0
|
|
341
|
+
? {
|
|
342
|
+
files: assets.map(({ path, contentType }) => ({
|
|
343
|
+
path,
|
|
344
|
+
contentType: contentType ?? null,
|
|
345
|
+
})),
|
|
346
|
+
notFound: 'none',
|
|
347
|
+
runWorkerFirst: true,
|
|
348
|
+
}
|
|
349
|
+
: null,
|
|
350
|
+
},
|
|
351
|
+
compatibilityDate: input.compatibilityDate ?? null,
|
|
352
|
+
compatibilityFlags: [...(input.compatibilityFlags ?? [])].sort(),
|
|
353
|
+
serviceBindings: [...(input.serviceBindings ?? [])]
|
|
354
|
+
.map(({ name, service }) => ({ name, service }))
|
|
355
|
+
.sort((left, right) => compareStable(`${left.name}\0${left.service}`, `${right.name}\0${right.service}`)),
|
|
356
|
+
vars: Object.entries(input.vars ?? {}).sort(([left], [right]) => compareStable(left, right)),
|
|
357
|
+
}));
|
|
358
|
+
return hash.digest('hex');
|
|
247
359
|
}
|
|
248
|
-
function
|
|
249
|
-
return
|
|
360
|
+
function compareStable(left, right) {
|
|
361
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
250
362
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
363
|
+
function updateHashFrame(hash, label, value) {
|
|
364
|
+
const bytes = typeof value === 'string' ? Buffer.from(value) : value;
|
|
365
|
+
hash.update(`${Buffer.byteLength(label)}:`);
|
|
366
|
+
hash.update(label);
|
|
367
|
+
hash.update(`:${bytes.byteLength}:`);
|
|
368
|
+
hash.update(bytes);
|
|
369
|
+
}
|
|
370
|
+
/** Preserve author vars while reserving the generation value owned by the adapter. */
|
|
371
|
+
export function managedDeploymentVars(vars, deploymentVersion) {
|
|
372
|
+
if (Object.hasOwn(vars ?? {}, DEPLOYMENT_VERSION_BINDING)) {
|
|
373
|
+
throw new Error(`astrale adapter: \`${DEPLOYMENT_VERSION_BINDING}\` is adapter-managed and cannot be set in \`vars\`.`);
|
|
374
|
+
}
|
|
375
|
+
if (deploymentVersion.length === 0) {
|
|
376
|
+
throw new TypeError('astrale adapter: deployment version must not be empty.');
|
|
377
|
+
}
|
|
378
|
+
return Object.freeze({ ...vars, [DEPLOYMENT_VERSION_BINDING]: deploymentVersion });
|
|
259
379
|
}
|
|
260
380
|
/** Serving MIME by file extension. The provider serves each asset with the type
|
|
261
381
|
* declared here VERBATIM — Cloudflare does NOT re-infer it from the path, so an
|
|
@@ -291,30 +411,22 @@ function assetMime(path) {
|
|
|
291
411
|
const dot = path.lastIndexOf('.');
|
|
292
412
|
return dot === -1 ? undefined : ASSET_MIME[path.slice(dot + 1).toLowerCase()];
|
|
293
413
|
}
|
|
294
|
-
/**
|
|
295
|
-
*
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
return
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
contentBase64: readFileSync(full).toString('base64'),
|
|
311
|
-
...(contentType ? { contentType } : {}),
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
walk(distClientDir);
|
|
317
|
-
return out;
|
|
414
|
+
/** Convert the materialized frontend into the services provider's immutable
|
|
415
|
+
* asset envelope. External frontends deliberately retain routes but no local
|
|
416
|
+
* files, so the callable worker and browser host remain independently swappable. */
|
|
417
|
+
export function managedFrontendAssets(frontend) {
|
|
418
|
+
if (frontend === undefined || frontend.artifact.source.kind === 'external') {
|
|
419
|
+
return Object.freeze([]);
|
|
420
|
+
}
|
|
421
|
+
return Object.freeze(frontend.files.map((file) => {
|
|
422
|
+
const path = `/${file.path}`;
|
|
423
|
+
const contentType = file.mediaType ?? assetMime(path);
|
|
424
|
+
return Object.freeze({
|
|
425
|
+
path,
|
|
426
|
+
contentBase64: Buffer.from(file.content).toString('base64'),
|
|
427
|
+
...(contentType === undefined ? {} : { contentType }),
|
|
428
|
+
});
|
|
429
|
+
}));
|
|
318
430
|
}
|
|
319
431
|
/** Semantic method address; independent of the installed domain's tree placement. */
|
|
320
432
|
export function servicesDeployMethodFor(origin) {
|
|
@@ -361,17 +473,18 @@ async function astraleCall(projectDir, params, call) {
|
|
|
361
473
|
function identityArgs(params) {
|
|
362
474
|
return params.identity ? ['--as', params.identity] : [];
|
|
363
475
|
}
|
|
364
|
-
/** Spawn the astrale CLI, feed optional stdin, return stdout
|
|
476
|
+
/** Spawn the astrale CLI, feed optional stdin, and return stdout. Throws with bounded diagnostics. */
|
|
365
477
|
async function runAstrale(bin, cwd, args, stdin) {
|
|
366
|
-
const { code,
|
|
478
|
+
const { code, stdout, stderr } = await new Promise((resolve, reject) => {
|
|
367
479
|
const child = spawn(bin, args, {
|
|
368
480
|
cwd,
|
|
369
481
|
stdio: [stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
370
482
|
});
|
|
371
|
-
let
|
|
372
|
-
|
|
373
|
-
child.
|
|
374
|
-
child.on('
|
|
483
|
+
let stdout = '';
|
|
484
|
+
let stderr = '';
|
|
485
|
+
child.stdout?.on('data', (b) => (stdout += b.toString()));
|
|
486
|
+
child.stderr?.on('data', (b) => (stderr += b.toString()));
|
|
487
|
+
child.on('exit', (c) => resolve({ code: c ?? 1, stdout, stderr }));
|
|
375
488
|
child.on('error', reject);
|
|
376
489
|
if (stdin !== undefined) {
|
|
377
490
|
child.stdin?.write(stdin);
|
|
@@ -379,11 +492,12 @@ async function runAstrale(bin, cwd, args, stdin) {
|
|
|
379
492
|
}
|
|
380
493
|
});
|
|
381
494
|
if (code !== 0) {
|
|
382
|
-
const
|
|
383
|
-
|
|
495
|
+
const diagnostics = `${stdout}\n${stderr}`.trim();
|
|
496
|
+
const authShaped = /AUTH_ERROR|expired|not signed in|credential/i.test(diagnostics);
|
|
497
|
+
throw new Error(`astrale ${args[0]} ${args[1] ?? ''} failed (exit ${code}): ${diagnostics.slice(0, 500)}` +
|
|
384
498
|
(authShaped ? `\nIs the astrale CLI installed and signed in? (astrale auth login)` : ''));
|
|
385
499
|
}
|
|
386
|
-
return
|
|
500
|
+
return stdout;
|
|
387
501
|
}
|
|
388
502
|
/** The user's astrale CLI: ~/.astrale/bin/astrale when present, else PATH. */
|
|
389
503
|
function astraleBin(_projectDir) {
|