@astrale-os/adapter-astrale 0.1.0

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `astrale(envs)` — the Astrale-managed deployment adapter.
3
+ *
4
+ * Ships the domain THROUGH the target instance's `services` domain instead of to
5
+ * the author's own cloud account. `deploy` builds the same single-module workerd
6
+ * bundle the Cloudflare adapter ships (shared codegen + `wrangler deploy
7
+ * --dry-run`), then:
8
+ *
9
+ * 1. hosts it on the instance's services domain — `CloudflareWorker.deploy
10
+ * { name, entry, modules, assets, vars }` → a public worker `url`;
11
+ * 2. (optional) pushes runtime secrets via the service's `setSecret`;
12
+ * 3. installs the domain on the instance from that `url`
13
+ * (`astrale domain install <url>` → the child kernel's `Root.installDomain`,
14
+ * which fetches + verifies the signed bundle and runs its `postInstall`).
15
+ *
16
+ * The admin control plane is NOT involved: hosting moved out of admin into the
17
+ * per-instance `services` domain (a separate bounded context). No Cloudflare
18
+ * account, no wrangler auth — the only credential is the author's Astrale
19
+ * identity, supplied by the `astrale` CLI session this adapter shells out to.
20
+ *
21
+ * REQUIRES the `services` domain installed on the target instance. An instance
22
+ * without it can't host managed deploys — `deploy` fails loud with that hint.
23
+ *
24
+ * `watch` is identical to the Cloudflare adapter's local dev (wrangler dev on
25
+ * localhost) — managed deploys change WHERE the worker runs, not how you iterate.
26
+ */
27
+ import type { DomainAdapter } from '@astrale-os/sdk';
28
+ import type { AstraleParams } from './params';
29
+ export declare function astrale(envs: Record<string, AstraleParams>): DomainAdapter<AstraleParams>;
30
+ /** Service name from the origin: first DNS label (e.g. `my-notes.example.dev` → `my-notes`). */
31
+ export declare function serviceNameFor(origin: string): string;
32
+ //# sourceMappingURL=astrale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"astrale.d.ts","sourceRoot":"","sources":["../src/astrale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAmBpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAK7C,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,aAAa,CAAC,aAAa,CAAC,CA2JzF;AAwBD,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrD"}
@@ -0,0 +1,255 @@
1
+ /**
2
+ * `astrale(envs)` — the Astrale-managed deployment adapter.
3
+ *
4
+ * Ships the domain THROUGH the target instance's `services` domain instead of to
5
+ * the author's own cloud account. `deploy` builds the same single-module workerd
6
+ * bundle the Cloudflare adapter ships (shared codegen + `wrangler deploy
7
+ * --dry-run`), then:
8
+ *
9
+ * 1. hosts it on the instance's services domain — `CloudflareWorker.deploy
10
+ * { name, entry, modules, assets, vars }` → a public worker `url`;
11
+ * 2. (optional) pushes runtime secrets via the service's `setSecret`;
12
+ * 3. installs the domain on the instance from that `url`
13
+ * (`astrale domain install <url>` → the child kernel's `Root.installDomain`,
14
+ * which fetches + verifies the signed bundle and runs its `postInstall`).
15
+ *
16
+ * The admin control plane is NOT involved: hosting moved out of admin into the
17
+ * per-instance `services` domain (a separate bounded context). No Cloudflare
18
+ * account, no wrangler auth — the only credential is the author's Astrale
19
+ * identity, supplied by the `astrale` CLI session this adapter shells out to.
20
+ *
21
+ * REQUIRES the `services` domain installed on the target instance. An instance
22
+ * without it can't host managed deploys — `deploy` fails loud with that hint.
23
+ *
24
+ * `watch` is identical to the Cloudflare adapter's local dev (wrangler dev on
25
+ * localhost) — managed deploys change WHERE the worker runs, not how you iterate.
26
+ */
27
+ // The shared Cloudflare-Workers build toolkit — the managed adapter ships the
28
+ // SAME bundle the direct `cloudflare` adapter does, then routes it through the
29
+ // platform instead of `wrangler deploy`.
30
+ import { buildClient, CLIENT_DIST_DIR, logTo, prepare, runWranglerBundle, runWranglerDev, } from '@astrale-os/adapter-cloudflare/build';
31
+ import { defineAdapter } from '@astrale-os/sdk';
32
+ import { loadDotenvFile } from '@astrale-os/sdk/cli';
33
+ import { spawn } from 'node:child_process';
34
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
35
+ import { join, relative } from 'node:path';
36
+ const DEFAULT_PORT = 8787;
37
+ const DEFAULT_SERVICES_ORIGIN = 'services.astrale.ai';
38
+ export function astrale(envs) {
39
+ return defineAdapter({
40
+ name: 'astrale',
41
+ envs,
42
+ // Local dev is unchanged: wrangler dev on localhost. Managed deploys change
43
+ // WHERE the worker ships, not the inner loop.
44
+ async watch(params, ctx) {
45
+ const { configPath } = await prepare({
46
+ ...(params.vars ? { vars: params.vars } : {}),
47
+ ...(params.host ? { host: params.host } : {}),
48
+ port: params.port ?? DEFAULT_PORT,
49
+ }, ctx, 'dev');
50
+ const handle = await runWranglerDev({
51
+ projectDir: ctx.projectDir,
52
+ configPath,
53
+ port: params.port ?? DEFAULT_PORT,
54
+ remote: false,
55
+ autoPickPort: params.port === undefined,
56
+ ...(params.host ? { ip: '0.0.0.0' } : {}),
57
+ onReload: ctx.onReload,
58
+ onLog: logTo(),
59
+ });
60
+ return { url: params.host ?? handle.url, stop: handle.stop };
61
+ },
62
+ // Config hot-regen for the local dev loop: same codegen as `watch` (the
63
+ // running `wrangler dev` reloads its generated config + entry itself) —
64
+ // the params mapping must stay identical to `watch`'s prepare call.
65
+ async regenerate(params, ctx) {
66
+ await prepare({
67
+ ...(params.vars ? { vars: params.vars } : {}),
68
+ ...(params.host ? { host: params.host } : {}),
69
+ port: params.port ?? DEFAULT_PORT,
70
+ }, ctx, 'dev');
71
+ },
72
+ async deploy(params, ctx) {
73
+ const log = logTo();
74
+ const instance = params.instance;
75
+ if (!instance) {
76
+ throw new Error(`astrale adapter: this env has no "instance" — managed deploys need the target ` +
77
+ `instance slug (e.g. prod: { instance: '<your-instance-slug>' }).`);
78
+ }
79
+ const servicesOrigin = params.servicesOrigin ?? DEFAULT_SERVICES_ORIGIN;
80
+ const name = params.name ?? serviceNameFor(ctx.domain.origin);
81
+ // Build + pack the client SPA: the services worker serves it under /ui.
82
+ let assetFiles = [];
83
+ if (ctx.clientDir) {
84
+ await buildClient(ctx.clientDir, ctx.projectDir, log);
85
+ assetFiles = readAssetFiles(join(ctx.projectDir, CLIENT_DIST_DIR));
86
+ if (assetFiles.length)
87
+ log(`packed client SPA (${assetFiles.length} files) — /ui ships managed`);
88
+ }
89
+ // 1. Codegen + bundle: the exact worker module a Cloudflare deploy would
90
+ // ship, produced locally with `wrangler deploy --dry-run` (no auth).
91
+ const { clientDir: _omitted, ...bundleCtx } = ctx;
92
+ const { configPath } = await prepare({}, bundleCtx, 'dev');
93
+ const outDir = join(ctx.projectDir, '.astrale', 'dist-managed');
94
+ const bundlePath = await runWranglerBundle({
95
+ projectDir: ctx.projectDir,
96
+ configPath,
97
+ outDir,
98
+ onLog: log,
99
+ });
100
+ const bundle = readFileSync(bundlePath);
101
+ // 2. Host it on the instance's services domain. `CloudflareWorker.deploy`
102
+ // upserts a worker by name on the platform's CF Workers-for-Platforms
103
+ // backend and returns its public URL. Payload rides STDIN — a bundle is
104
+ // megabytes of base64, far past argv limits.
105
+ log(`deploying "${name}" to the services domain on "${instance}"…`);
106
+ const deployed = (await astraleCall(ctx.projectDir, params, {
107
+ instance,
108
+ path: `/${servicesOrigin}/class.CloudflareWorker/deploy`,
109
+ data: {
110
+ name,
111
+ entry: 'index.mjs',
112
+ modules: [{ name: 'index.mjs', contentBase64: bundle.toString('base64'), kind: 'esm' }],
113
+ ...(assetFiles.length
114
+ ? {
115
+ assets: {
116
+ files: assetFiles,
117
+ notFound: 'single-page-application',
118
+ runWorkerFirst: true,
119
+ },
120
+ }
121
+ : {}),
122
+ ...(params.vars ? { vars: params.vars } : {}),
123
+ },
124
+ viaStdin: true,
125
+ }));
126
+ if (deployed.error || !deployed.url) {
127
+ throw new Error(`services deploy failed: state=${deployed.state ?? '?'} error=${deployed.error ?? '?'}. ` +
128
+ `Is the services domain installed on "${instance}"?`);
129
+ }
130
+ const url = deployed.url;
131
+ // 3. Author runtime secrets: read the env's dotenv locally and push each
132
+ // via the service's write-only `setSecret` (values ride stdin, never argv).
133
+ if (params.secrets) {
134
+ const secrets = loadDotenvFile(join(ctx.projectDir, params.secrets));
135
+ const keys = Object.keys(secrets);
136
+ if (keys.length)
137
+ log(`setting ${keys.length} secret(s) from ${params.secrets}…`);
138
+ for (const [key, value] of Object.entries(secrets)) {
139
+ await astraleCall(ctx.projectDir, params, {
140
+ instance,
141
+ path: `/${servicesOrigin}/services/${name}/setSecret`,
142
+ data: { name: key, value },
143
+ viaStdin: true,
144
+ });
145
+ }
146
+ }
147
+ // 4. Install the domain on the instance from the hosted URL. The child
148
+ // kernel fetches the signed bundle, verifies it, and runs its postInstall.
149
+ log(`installing "${ctx.domain.origin}" on "${instance}" from ${url}…`);
150
+ await runAstrale(astraleBin(ctx.projectDir), ctx.projectDir, [
151
+ 'instance',
152
+ 'install',
153
+ url,
154
+ '-i',
155
+ instance,
156
+ '--json',
157
+ '--timeout',
158
+ '240000',
159
+ ]);
160
+ return {
161
+ url,
162
+ nextSteps: ` installed on "${instance}" — call it:\n` +
163
+ ` astrale call "/${ctx.domain.origin}/..." -i ${instance}`,
164
+ };
165
+ },
166
+ secretsFile(params) {
167
+ return params.secrets;
168
+ },
169
+ });
170
+ }
171
+ /** Walk a built SPA dir into the services schema's `AssetFileSchema[]` (paths rooted at `/`). */
172
+ function readAssetFiles(distClientDir) {
173
+ const out = [];
174
+ if (!existsSync(distClientDir))
175
+ return out;
176
+ const walk = (dir) => {
177
+ for (const entry of readdirSync(dir)) {
178
+ const full = join(dir, entry);
179
+ if (statSync(full).isDirectory())
180
+ walk(full);
181
+ else {
182
+ out.push({
183
+ path: `/${relative(distClientDir, full).split('\\').join('/')}`,
184
+ contentBase64: readFileSync(full).toString('base64'),
185
+ });
186
+ }
187
+ }
188
+ };
189
+ walk(distClientDir);
190
+ return out;
191
+ }
192
+ /** Service name from the origin: first DNS label (e.g. `my-notes.example.dev` → `my-notes`). */
193
+ export function serviceNameFor(origin) {
194
+ const label = origin.split('.')[0] ?? origin;
195
+ return label.toLowerCase().replace(/[^a-z0-9-]+/g, '-');
196
+ }
197
+ /**
198
+ * Invoke the user's `astrale` CLI for one platform call (against the target
199
+ * INSTANCE) and parse its JSON output. The CLI owns identity (IdP session,
200
+ * audience scoping, delegation minting) — reimplementing that here would fork
201
+ * the trust path.
202
+ */
203
+ async function astraleCall(projectDir, params, call) {
204
+ // Saga-sized timeout: a cold managed deploy regularly exceeds the CLI's 30s
205
+ // default. Target the instance (`-i`), not admin — the services domain lives
206
+ // on the instance.
207
+ const args = ['call', call.path, '-i', call.instance, '--json', '--timeout', '240000'];
208
+ if (params.identity)
209
+ args.push('--as', params.identity);
210
+ const payload = JSON.stringify(call.data);
211
+ if (!call.viaStdin)
212
+ args.push('--data', payload);
213
+ const out = await runAstrale(astraleBin(projectDir), projectDir, args, call.viaStdin ? payload : undefined);
214
+ try {
215
+ return JSON.parse(out);
216
+ }
217
+ catch {
218
+ throw new Error(`astrale call ${call.path}: could not parse CLI output: ${out.slice(0, 300)}`);
219
+ }
220
+ }
221
+ /** Spawn the astrale CLI, feed optional stdin, return stdout+stderr. Throws on non-zero exit. */
222
+ async function runAstrale(bin, cwd, args, stdin) {
223
+ const { code, out } = await new Promise((resolve, reject) => {
224
+ const child = spawn(bin, args, {
225
+ cwd,
226
+ stdio: [stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
227
+ });
228
+ let out = '';
229
+ child.stdout?.on('data', (b) => (out += b.toString()));
230
+ child.stderr?.on('data', (b) => (out += b.toString()));
231
+ child.on('exit', (c) => resolve({ code: c ?? 1, out }));
232
+ child.on('error', reject);
233
+ if (stdin !== undefined) {
234
+ child.stdin?.write(stdin);
235
+ child.stdin?.end();
236
+ }
237
+ });
238
+ if (code !== 0) {
239
+ const authShaped = /AUTH_ERROR|expired|not signed in|credential/i.test(out);
240
+ throw new Error(`astrale ${args[0]} ${args[1] ?? ''} failed (exit ${code}): ${out.trim().slice(0, 500)}` +
241
+ (authShaped ? `\nIs the astrale CLI installed and signed in? (astrale auth login)` : ''));
242
+ }
243
+ return out;
244
+ }
245
+ /** The user's astrale CLI: ~/.astrale/bin/astrale when present, else PATH. */
246
+ function astraleBin(_projectDir) {
247
+ const home = process.env.HOME;
248
+ if (home) {
249
+ const installed = join(home, '.astrale', 'bin', 'astrale');
250
+ if (existsSync(installed))
251
+ return installed;
252
+ }
253
+ return 'astrale';
254
+ }
255
+ //# sourceMappingURL=astrale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"astrale.js","sourceRoot":"","sources":["../src/astrale.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@astrale-os/adapter-astrale` — deploy an Astrale domain as a MANAGED service
3
+ * through the Astrale platform (no Cloudflare account, no wrangler auth — the
4
+ * only credential is your `astrale auth login` session).
5
+ *
6
+ * import { astrale } from '@astrale-os/adapter-astrale'
7
+ *
8
+ * adapter: astrale({
9
+ * dev: { secrets: '.env.dev' }, // local wrangler dev, unchanged
10
+ * prod: { instance: '<your-instance-slug>' },
11
+ * })
12
+ *
13
+ * It builds the SAME Cloudflare Workers bundle the `cloudflare` adapter ships
14
+ * (via `@astrale-os/adapter-cloudflare/build`), then hosts it on the target
15
+ * instance's `services` domain and installs the domain from the resulting URL.
16
+ * For deploying to YOUR OWN Cloudflare account instead, use
17
+ * `@astrale-os/adapter-cloudflare`.
18
+ */
19
+ export { astrale } from './astrale';
20
+ export type { AstraleParams } from './params';
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@astrale-os/adapter-astrale` — deploy an Astrale domain as a MANAGED service
3
+ * through the Astrale platform (no Cloudflare account, no wrangler auth — the
4
+ * only credential is your `astrale auth login` session).
5
+ *
6
+ * import { astrale } from '@astrale-os/adapter-astrale'
7
+ *
8
+ * adapter: astrale({
9
+ * dev: { secrets: '.env.dev' }, // local wrangler dev, unchanged
10
+ * prod: { instance: '<your-instance-slug>' },
11
+ * })
12
+ *
13
+ * It builds the SAME Cloudflare Workers bundle the `cloudflare` adapter ships
14
+ * (via `@astrale-os/adapter-cloudflare/build`), then hosts it on the target
15
+ * instance's `services` domain and installs the domain from the resulting URL.
16
+ * For deploying to YOUR OWN Cloudflare account instead, use
17
+ * `@astrale-os/adapter-cloudflare`.
18
+ */
19
+ export { astrale } from './astrale';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `AstraleParams` — the provider-typed config for the Astrale-managed adapter.
3
+ *
4
+ * One type covers every env; per-env differences are just optional fields. The
5
+ * same `{ dev, prod, canary, … }` map a developer writes in `astrale.config.ts`
6
+ * is `Record<string, AstraleParams>`.
7
+ *
8
+ * `watch` is local `wrangler dev` (identical to the Cloudflare adapter's dev);
9
+ * `deploy` ships the bundle through the target instance's `services` domain and
10
+ * installs it — so `instance` is required for `deploy`, ignored by `watch`.
11
+ */
12
+ export interface AstraleParams {
13
+ /**
14
+ * Target instance slug the domain installs onto (e.g. `'bryan-e2e5'`).
15
+ * Required for `deploy` (managed publish + install); ignored by `watch`
16
+ * (local dev) — so dev-only envs may omit it.
17
+ */
18
+ instance?: string;
19
+ /** Service name on the platform. Default: the origin's first DNS label. */
20
+ name?: string;
21
+ /**
22
+ * Origin of the `services` domain installed on the target instance — where the
23
+ * worker is hosted (`CloudflareWorker.deploy`). Default `'services.astrale.ai'`.
24
+ */
25
+ servicesOrigin?: string;
26
+ /** @deprecated Unused since hosting moved to the per-instance services domain. */
27
+ adminUrl?: string;
28
+ /** @deprecated Unused since hosting moved to the per-instance services domain. */
29
+ region?: string;
30
+ /** CLI identity (`--as`) for the deploy/install calls. Default: the CLI's active identity. */
31
+ identity?: string;
32
+ /** Gitignored secrets file. `watch`: injected into local wrangler dev. `deploy`: shipped on the install call and applied to the managed service env (encrypted at rest platform-side; platform keys win precedence). */
33
+ secrets?: string;
34
+ /** Local dev port for `wrangler dev`. Default 8787. */
35
+ port?: number;
36
+ /**
37
+ * Public URL the dev server is reached through (tunnel / sandbox preview /
38
+ * reverse proxy), e.g. `'https://my-box.preview.dev'`. Sets BOTH effects an
39
+ * off-localhost dev needs: wrangler binds 0.0.0.0 (reachable from outside)
40
+ * and `WORKER_URL` is pinned to this URL so the worker's per-request-Host
41
+ * identity (`iss`) doesn't drift to the proxy's internal hostname.
42
+ * Usually injected by `pnpm dev --host <url>` rather than written here.
43
+ */
44
+ host?: string;
45
+ /** Extra plain vars for local `watch`. */
46
+ vars?: Record<string, string>;
47
+ }
48
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../src/params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wNAAwN;IACxN,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC9B"}
package/dist/params.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.js","sourceRoot":"","sources":["../src/params.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@astrale-os/adapter-astrale",
3
+ "version": "0.1.0",
4
+ "description": "Deploy an Astrale domain as a managed service through the Astrale platform",
5
+ "keywords": [
6
+ "adapter",
7
+ "astrale",
8
+ "domain",
9
+ "managed",
10
+ "workers"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Astrale",
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "template"
18
+ ],
19
+ "type": "module",
20
+ "main": "./src/index.ts",
21
+ "types": "./src/index.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "dependencies": {
30
+ "@astrale-os/adapter-cloudflare": "^0.1.9",
31
+ "@astrale-os/sdk": "^0.1.7"
32
+ },
33
+ "devDependencies": {
34
+ "@astrale-os/ox": ">=0.1.0 <1.0.0",
35
+ "@types/node": "^22.0.0",
36
+ "@typescript/native-preview": "latest",
37
+ "oxfmt": "latest",
38
+ "oxlint": "latest",
39
+ "typescript": "~6.0.1-rc",
40
+ "vitest": "^3.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "scripts": {
46
+ "typecheck": "tsgo --noEmit",
47
+ "test": "vitest run",
48
+ "lint": "oxlint .",
49
+ "format": "pnpm exec oxfmt --write .",
50
+ "format:check": "pnpm exec oxfmt --check .",
51
+ "build": "rm -rf dist && tsgo"
52
+ }
53
+ }
package/src/astrale.ts ADDED
@@ -0,0 +1,309 @@
1
+ /**
2
+ * `astrale(envs)` — the Astrale-managed deployment adapter.
3
+ *
4
+ * Ships the domain THROUGH the target instance's `services` domain instead of to
5
+ * the author's own cloud account. `deploy` builds the same single-module workerd
6
+ * bundle the Cloudflare adapter ships (shared codegen + `wrangler deploy
7
+ * --dry-run`), then:
8
+ *
9
+ * 1. hosts it on the instance's services domain — `CloudflareWorker.deploy
10
+ * { name, entry, modules, assets, vars }` → a public worker `url`;
11
+ * 2. (optional) pushes runtime secrets via the service's `setSecret`;
12
+ * 3. installs the domain on the instance from that `url`
13
+ * (`astrale domain install <url>` → the child kernel's `Root.installDomain`,
14
+ * which fetches + verifies the signed bundle and runs its `postInstall`).
15
+ *
16
+ * The admin control plane is NOT involved: hosting moved out of admin into the
17
+ * per-instance `services` domain (a separate bounded context). No Cloudflare
18
+ * account, no wrangler auth — the only credential is the author's Astrale
19
+ * identity, supplied by the `astrale` CLI session this adapter shells out to.
20
+ *
21
+ * REQUIRES the `services` domain installed on the target instance. An instance
22
+ * without it can't host managed deploys — `deploy` fails loud with that hint.
23
+ *
24
+ * `watch` is identical to the Cloudflare adapter's local dev (wrangler dev on
25
+ * localhost) — managed deploys change WHERE the worker runs, not how you iterate.
26
+ */
27
+
28
+ import type { DomainAdapter } from '@astrale-os/sdk'
29
+
30
+ // The shared Cloudflare-Workers build toolkit — the managed adapter ships the
31
+ // SAME bundle the direct `cloudflare` adapter does, then routes it through the
32
+ // platform instead of `wrangler deploy`.
33
+ import {
34
+ buildClient,
35
+ CLIENT_DIST_DIR,
36
+ logTo,
37
+ prepare,
38
+ runWranglerBundle,
39
+ runWranglerDev,
40
+ } from '@astrale-os/adapter-cloudflare/build'
41
+ import { defineAdapter } from '@astrale-os/sdk'
42
+ import { loadDotenvFile } from '@astrale-os/sdk/cli'
43
+ import { spawn } from 'node:child_process'
44
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
45
+ import { join, relative } from 'node:path'
46
+
47
+ import type { AstraleParams } from './params'
48
+
49
+ const DEFAULT_PORT = 8787
50
+ const DEFAULT_SERVICES_ORIGIN = 'services.astrale.ai'
51
+
52
+ export function astrale(envs: Record<string, AstraleParams>): DomainAdapter<AstraleParams> {
53
+ return defineAdapter<AstraleParams>({
54
+ name: 'astrale',
55
+ envs,
56
+
57
+ // Local dev is unchanged: wrangler dev on localhost. Managed deploys change
58
+ // WHERE the worker ships, not the inner loop.
59
+ async watch(params, ctx) {
60
+ const { configPath } = await prepare(
61
+ {
62
+ ...(params.vars ? { vars: params.vars } : {}),
63
+ ...(params.host ? { host: params.host } : {}),
64
+ port: params.port ?? DEFAULT_PORT,
65
+ },
66
+ ctx,
67
+ 'dev',
68
+ )
69
+ const handle = await runWranglerDev({
70
+ projectDir: ctx.projectDir,
71
+ configPath,
72
+ port: params.port ?? DEFAULT_PORT,
73
+ remote: false,
74
+ autoPickPort: params.port === undefined,
75
+ ...(params.host ? { ip: '0.0.0.0' } : {}),
76
+ onReload: ctx.onReload,
77
+ onLog: logTo(),
78
+ })
79
+ return { url: params.host ?? handle.url, stop: handle.stop }
80
+ },
81
+
82
+ // Config hot-regen for the local dev loop: same codegen as `watch` (the
83
+ // running `wrangler dev` reloads its generated config + entry itself) —
84
+ // the params mapping must stay identical to `watch`'s prepare call.
85
+ async regenerate(params, ctx) {
86
+ await prepare(
87
+ {
88
+ ...(params.vars ? { vars: params.vars } : {}),
89
+ ...(params.host ? { host: params.host } : {}),
90
+ port: params.port ?? DEFAULT_PORT,
91
+ },
92
+ ctx,
93
+ 'dev',
94
+ )
95
+ },
96
+
97
+ async deploy(params, ctx) {
98
+ const log = logTo()
99
+ const instance = params.instance
100
+ if (!instance) {
101
+ throw new Error(
102
+ `astrale adapter: this env has no "instance" — managed deploys need the target ` +
103
+ `instance slug (e.g. prod: { instance: '<your-instance-slug>' }).`,
104
+ )
105
+ }
106
+ const servicesOrigin = params.servicesOrigin ?? DEFAULT_SERVICES_ORIGIN
107
+ const name = params.name ?? serviceNameFor(ctx.domain.origin)
108
+
109
+ // Build + pack the client SPA: the services worker serves it under /ui.
110
+ let assetFiles: AssetFile[] = []
111
+ if (ctx.clientDir) {
112
+ await buildClient(ctx.clientDir, ctx.projectDir, log)
113
+ assetFiles = readAssetFiles(join(ctx.projectDir, CLIENT_DIST_DIR))
114
+ if (assetFiles.length)
115
+ log(`packed client SPA (${assetFiles.length} files) — /ui ships managed`)
116
+ }
117
+
118
+ // 1. Codegen + bundle: the exact worker module a Cloudflare deploy would
119
+ // ship, produced locally with `wrangler deploy --dry-run` (no auth).
120
+ const { clientDir: _omitted, ...bundleCtx } = ctx
121
+ const { configPath } = await prepare({}, bundleCtx, 'dev')
122
+ const outDir = join(ctx.projectDir, '.astrale', 'dist-managed')
123
+ const bundlePath = await runWranglerBundle({
124
+ projectDir: ctx.projectDir,
125
+ configPath,
126
+ outDir,
127
+ onLog: log,
128
+ })
129
+ const bundle = readFileSync(bundlePath)
130
+
131
+ // 2. Host it on the instance's services domain. `CloudflareWorker.deploy`
132
+ // upserts a worker by name on the platform's CF Workers-for-Platforms
133
+ // backend and returns its public URL. Payload rides STDIN — a bundle is
134
+ // megabytes of base64, far past argv limits.
135
+ log(`deploying "${name}" to the services domain on "${instance}"…`)
136
+ const deployed = (await astraleCall(ctx.projectDir, params, {
137
+ instance,
138
+ path: `/${servicesOrigin}/class.CloudflareWorker/deploy`,
139
+ data: {
140
+ name,
141
+ entry: 'index.mjs',
142
+ modules: [{ name: 'index.mjs', contentBase64: bundle.toString('base64'), kind: 'esm' }],
143
+ ...(assetFiles.length
144
+ ? {
145
+ assets: {
146
+ files: assetFiles,
147
+ notFound: 'single-page-application',
148
+ runWorkerFirst: true,
149
+ },
150
+ }
151
+ : {}),
152
+ ...(params.vars ? { vars: params.vars } : {}),
153
+ },
154
+ viaStdin: true,
155
+ })) as { name?: string; url?: string; state?: string; error?: string | null }
156
+
157
+ if (deployed.error || !deployed.url) {
158
+ throw new Error(
159
+ `services deploy failed: state=${deployed.state ?? '?'} error=${deployed.error ?? '?'}. ` +
160
+ `Is the services domain installed on "${instance}"?`,
161
+ )
162
+ }
163
+ const url = deployed.url
164
+
165
+ // 3. Author runtime secrets: read the env's dotenv locally and push each
166
+ // via the service's write-only `setSecret` (values ride stdin, never argv).
167
+ if (params.secrets) {
168
+ const secrets = loadDotenvFile(join(ctx.projectDir, params.secrets))
169
+ const keys = Object.keys(secrets)
170
+ if (keys.length) log(`setting ${keys.length} secret(s) from ${params.secrets}…`)
171
+ for (const [key, value] of Object.entries(secrets)) {
172
+ await astraleCall(ctx.projectDir, params, {
173
+ instance,
174
+ path: `/${servicesOrigin}/services/${name}/setSecret`,
175
+ data: { name: key, value },
176
+ viaStdin: true,
177
+ })
178
+ }
179
+ }
180
+
181
+ // 4. Install the domain on the instance from the hosted URL. The child
182
+ // kernel fetches the signed bundle, verifies it, and runs its postInstall.
183
+ log(`installing "${ctx.domain.origin}" on "${instance}" from ${url}…`)
184
+ await runAstrale(astraleBin(ctx.projectDir), ctx.projectDir, [
185
+ 'instance',
186
+ 'install',
187
+ url,
188
+ '-i',
189
+ instance,
190
+ '--json',
191
+ '--timeout',
192
+ '240000',
193
+ ])
194
+
195
+ return {
196
+ url,
197
+ nextSteps:
198
+ ` installed on "${instance}" — call it:\n` +
199
+ ` astrale call "/${ctx.domain.origin}/..." -i ${instance}`,
200
+ }
201
+ },
202
+
203
+ secretsFile(params) {
204
+ return params.secrets
205
+ },
206
+ })
207
+ }
208
+
209
+ type AssetFile = { path: string; contentBase64: string }
210
+
211
+ /** Walk a built SPA dir into the services schema's `AssetFileSchema[]` (paths rooted at `/`). */
212
+ function readAssetFiles(distClientDir: string): AssetFile[] {
213
+ const out: AssetFile[] = []
214
+ if (!existsSync(distClientDir)) return out
215
+ const walk = (dir: string): void => {
216
+ for (const entry of readdirSync(dir)) {
217
+ const full = join(dir, entry)
218
+ if (statSync(full).isDirectory()) walk(full)
219
+ else {
220
+ out.push({
221
+ path: `/${relative(distClientDir, full).split('\\').join('/')}`,
222
+ contentBase64: readFileSync(full).toString('base64'),
223
+ })
224
+ }
225
+ }
226
+ }
227
+ walk(distClientDir)
228
+ return out
229
+ }
230
+
231
+ /** Service name from the origin: first DNS label (e.g. `my-notes.example.dev` → `my-notes`). */
232
+ export function serviceNameFor(origin: string): string {
233
+ const label = origin.split('.')[0] ?? origin
234
+ return label.toLowerCase().replace(/[^a-z0-9-]+/g, '-')
235
+ }
236
+
237
+ /**
238
+ * Invoke the user's `astrale` CLI for one platform call (against the target
239
+ * INSTANCE) and parse its JSON output. The CLI owns identity (IdP session,
240
+ * audience scoping, delegation minting) — reimplementing that here would fork
241
+ * the trust path.
242
+ */
243
+ async function astraleCall(
244
+ projectDir: string,
245
+ params: AstraleParams,
246
+ call: { instance: string; path: string; data: unknown; viaStdin?: boolean },
247
+ ): Promise<unknown> {
248
+ // Saga-sized timeout: a cold managed deploy regularly exceeds the CLI's 30s
249
+ // default. Target the instance (`-i`), not admin — the services domain lives
250
+ // on the instance.
251
+ const args = ['call', call.path, '-i', call.instance, '--json', '--timeout', '240000']
252
+ if (params.identity) args.push('--as', params.identity)
253
+ const payload = JSON.stringify(call.data)
254
+ if (!call.viaStdin) args.push('--data', payload)
255
+
256
+ const out = await runAstrale(
257
+ astraleBin(projectDir),
258
+ projectDir,
259
+ args,
260
+ call.viaStdin ? payload : undefined,
261
+ )
262
+ try {
263
+ return JSON.parse(out)
264
+ } catch {
265
+ throw new Error(`astrale call ${call.path}: could not parse CLI output: ${out.slice(0, 300)}`)
266
+ }
267
+ }
268
+
269
+ /** Spawn the astrale CLI, feed optional stdin, return stdout+stderr. Throws on non-zero exit. */
270
+ async function runAstrale(
271
+ bin: string,
272
+ cwd: string,
273
+ args: string[],
274
+ stdin?: string,
275
+ ): Promise<string> {
276
+ const { code, out } = await new Promise<{ code: number; out: string }>((resolve, reject) => {
277
+ const child = spawn(bin, args, {
278
+ cwd,
279
+ stdio: [stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
280
+ })
281
+ let out = ''
282
+ child.stdout?.on('data', (b: Buffer) => (out += b.toString()))
283
+ child.stderr?.on('data', (b: Buffer) => (out += b.toString()))
284
+ child.on('exit', (c) => resolve({ code: c ?? 1, out }))
285
+ child.on('error', reject)
286
+ if (stdin !== undefined) {
287
+ child.stdin?.write(stdin)
288
+ child.stdin?.end()
289
+ }
290
+ })
291
+ if (code !== 0) {
292
+ const authShaped = /AUTH_ERROR|expired|not signed in|credential/i.test(out)
293
+ throw new Error(
294
+ `astrale ${args[0]} ${args[1] ?? ''} failed (exit ${code}): ${out.trim().slice(0, 500)}` +
295
+ (authShaped ? `\nIs the astrale CLI installed and signed in? (astrale auth login)` : ''),
296
+ )
297
+ }
298
+ return out
299
+ }
300
+
301
+ /** The user's astrale CLI: ~/.astrale/bin/astrale when present, else PATH. */
302
+ function astraleBin(_projectDir: string): string {
303
+ const home = process.env.HOME
304
+ if (home) {
305
+ const installed = join(home, '.astrale', 'bin', 'astrale')
306
+ if (existsSync(installed)) return installed
307
+ }
308
+ return 'astrale'
309
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@astrale-os/adapter-astrale` — deploy an Astrale domain as a MANAGED service
3
+ * through the Astrale platform (no Cloudflare account, no wrangler auth — the
4
+ * only credential is your `astrale auth login` session).
5
+ *
6
+ * import { astrale } from '@astrale-os/adapter-astrale'
7
+ *
8
+ * adapter: astrale({
9
+ * dev: { secrets: '.env.dev' }, // local wrangler dev, unchanged
10
+ * prod: { instance: '<your-instance-slug>' },
11
+ * })
12
+ *
13
+ * It builds the SAME Cloudflare Workers bundle the `cloudflare` adapter ships
14
+ * (via `@astrale-os/adapter-cloudflare/build`), then hosts it on the target
15
+ * instance's `services` domain and installs the domain from the resulting URL.
16
+ * For deploying to YOUR OWN Cloudflare account instead, use
17
+ * `@astrale-os/adapter-cloudflare`.
18
+ */
19
+
20
+ export { astrale } from './astrale'
21
+ export type { AstraleParams } from './params'
package/src/params.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `AstraleParams` — the provider-typed config for the Astrale-managed adapter.
3
+ *
4
+ * One type covers every env; per-env differences are just optional fields. The
5
+ * same `{ dev, prod, canary, … }` map a developer writes in `astrale.config.ts`
6
+ * is `Record<string, AstraleParams>`.
7
+ *
8
+ * `watch` is local `wrangler dev` (identical to the Cloudflare adapter's dev);
9
+ * `deploy` ships the bundle through the target instance's `services` domain and
10
+ * installs it — so `instance` is required for `deploy`, ignored by `watch`.
11
+ */
12
+ export interface AstraleParams {
13
+ /**
14
+ * Target instance slug the domain installs onto (e.g. `'bryan-e2e5'`).
15
+ * Required for `deploy` (managed publish + install); ignored by `watch`
16
+ * (local dev) — so dev-only envs may omit it.
17
+ */
18
+ instance?: string
19
+ /** Service name on the platform. Default: the origin's first DNS label. */
20
+ name?: string
21
+ /**
22
+ * Origin of the `services` domain installed on the target instance — where the
23
+ * worker is hosted (`CloudflareWorker.deploy`). Default `'services.astrale.ai'`.
24
+ */
25
+ servicesOrigin?: string
26
+ /** @deprecated Unused since hosting moved to the per-instance services domain. */
27
+ adminUrl?: string
28
+ /** @deprecated Unused since hosting moved to the per-instance services domain. */
29
+ region?: string
30
+ /** CLI identity (`--as`) for the deploy/install calls. Default: the CLI's active identity. */
31
+ identity?: string
32
+ /** Gitignored secrets file. `watch`: injected into local wrangler dev. `deploy`: shipped on the install call and applied to the managed service env (encrypted at rest platform-side; platform keys win precedence). */
33
+ secrets?: string
34
+ /** Local dev port for `wrangler dev`. Default 8787. */
35
+ port?: number
36
+ /**
37
+ * Public URL the dev server is reached through (tunnel / sandbox preview /
38
+ * reverse proxy), e.g. `'https://my-box.preview.dev'`. Sets BOTH effects an
39
+ * off-localhost dev needs: wrangler binds 0.0.0.0 (reachable from outside)
40
+ * and `WORKER_URL` is pinned to this URL so the worker's per-request-Host
41
+ * identity (`iss`) doesn't drift to the proxy's internal hostname.
42
+ * Usually injected by `pnpm dev --host <url>` rather than written here.
43
+ */
44
+ host?: string
45
+ /** Extra plain vars for local `watch`. */
46
+ vars?: Record<string, string>
47
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * astrale.config.ts — binds the worker-safe domain (`domain.ts`) to the managed
3
+ * Astrale deploy adapter for the `astrale-domain` CLI. This is a NODE-only
4
+ * module; the generated worker imports `domain.ts` directly, never this file.
5
+ *
6
+ * This variant ships THROUGH the Astrale platform (the managed `astrale`
7
+ * adapter) — no Cloudflare account needed.
8
+ *
9
+ * pnpm dev # wrangler dev → prints a local URL
10
+ * astrale domain install <url> --direct # mount the dev URL on an instance
11
+ * pnpm prod # managed deploy → prints a URL + installs
12
+ */
13
+ import { astrale } from '@astrale-os/adapter-astrale'
14
+ import { deploy } from '@astrale-os/sdk'
15
+ // Have your own Cloudflare account and prefer deploying there directly? Swap
16
+ // the adapter — same bundle, shipped with wrangler under YOUR account (then
17
+ // mount it yourself: `astrale domain install <url> --direct`):
18
+ //
19
+ // import { cloudflare } from '@astrale-os/adapter-cloudflare'
20
+ // export default deploy(domain, cloudflare({
21
+ // dev: { secrets: '.env.dev' },
22
+ // prod: { route: 'astrale-domain.example.dev', secrets: '.env.prod' },
23
+ // }))
24
+
25
+ import { domain } from './domain'
26
+
27
+ export default deploy(
28
+ domain,
29
+ astrale({
30
+ // Local dev: `wrangler dev`. No route → URL is http://localhost:8787.
31
+ dev: { secrets: '.env.dev' },
32
+ // Managed deploy: `pnpm prod` publishes the bundle through the platform and
33
+ // installs the domain on this instance. The slug comes from
34
+ // `astrale instance create <slug>` / `astrale instance status`.
35
+ prod: { instance: 'my-instance-slug' },
36
+ // Author secrets ship via `secrets: '.env.prod'` on any env (encrypted at
37
+ // rest platform-side, re-applied on redeploys; omit = keep, `{}` = clear).
38
+ }),
39
+ )