@astrale-os/adapter-cloudflare 0.5.0-beta.71 → 0.5.0-beta.72

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 (54) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -0
  3. package/dist/adapter.d.ts +2 -4
  4. package/dist/adapter.js +5 -106
  5. package/dist/build/artifact.js +7 -5
  6. package/dist/build/{frontend-v1.d.ts → frontend/files.d.ts} +0 -9
  7. package/dist/build/frontend/files.js +60 -0
  8. package/dist/build/index.d.ts +2 -2
  9. package/dist/build/index.js +2 -2
  10. package/dist/build/worker/execution.prepare.js +1 -1
  11. package/dist/deploy/deploy.d.ts +2 -1
  12. package/dist/deploy/deploy.js +1 -1
  13. package/dist/deploy/output.d.ts +2 -0
  14. package/dist/deploy/output.js +10 -0
  15. package/dist/development/develop.d.ts +15 -0
  16. package/dist/development/develop.js +267 -0
  17. package/dist/development/frontend.d.ts +11 -0
  18. package/dist/{build/frontend-v1.js → development/frontend.js} +31 -55
  19. package/dist/development/host.d.ts +2 -0
  20. package/dist/development/host.js +46 -0
  21. package/dist/development/port.d.ts +1 -0
  22. package/dist/development/port.js +12 -0
  23. package/dist/development/readiness.d.ts +14 -0
  24. package/dist/development/readiness.js +71 -0
  25. package/dist/development/resolved-fetch.d.ts +18 -0
  26. package/dist/development/resolved-fetch.js +78 -0
  27. package/dist/development/worker-output.d.ts +2 -0
  28. package/dist/development/worker-output.js +25 -0
  29. package/dist/{build/wrangler/development.d.ts → development/worker.d.ts} +5 -5
  30. package/dist/{build/wrangler/development.js → development/worker.js} +24 -13
  31. package/dist/identity/source.js +37 -8
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +2 -1
  34. package/dist/params.d.ts +3 -3
  35. package/dist/{build/wrangler → wrangler}/bundle.js +0 -2
  36. package/dist/{build/wrangler → wrangler}/deploy.js +1 -1
  37. package/dist/wrangler/index.d.ts +5 -0
  38. package/dist/wrangler/index.js +5 -0
  39. package/package.json +2 -2
  40. package/template/astrale.config.ts +6 -6
  41. package/dist/build/parse-output.d.ts +0 -26
  42. package/dist/build/parse-output.js +0 -57
  43. package/dist/build/wrangler-cli.d.ts +0 -6
  44. package/dist/build/wrangler-cli.js +0 -6
  45. /package/dist/{build/wrangler → wrangler}/bundle.d.ts +0 -0
  46. /package/dist/{build/wrangler → wrangler}/deploy.d.ts +0 -0
  47. /package/dist/{build/wrangler → wrangler}/process.d.ts +0 -0
  48. /package/dist/{build/wrangler → wrangler}/process.js +0 -0
  49. /package/dist/{build/wrangler → wrangler}/secrets.d.ts +0 -0
  50. /package/dist/{build/wrangler → wrangler}/secrets.js +0 -0
  51. /package/dist/{build/wrangler → wrangler}/status.d.ts +0 -0
  52. /package/dist/{build/wrangler → wrangler}/status.js +0 -0
  53. /package/dist/{build/wrangler → wrangler}/type-generation.d.ts +0 -0
  54. /package/dist/{build/wrangler → wrangler}/type-generation.js +0 -0
@@ -0,0 +1,11 @@
1
+ import type { Build } from '@astrale-os/sdk/deployment/build';
2
+ export interface FrontendHandle {
3
+ readonly url: string;
4
+ readonly finished: Promise<void>;
5
+ stop(): Promise<void>;
6
+ }
7
+ export declare function startFrontend(input: {
8
+ readonly frontend: Build['frontend'];
9
+ readonly projectDir: string;
10
+ readonly onLog?: (line: string) => void;
11
+ }): Promise<FrontendHandle | undefined>;
@@ -1,26 +1,7 @@
1
- import { frontend as frontendBuild } from '@astrale-os/sdk/deployment/build';
2
1
  import { spawn } from 'node:child_process';
3
2
  import { existsSync } from 'node:fs';
4
- import { readFile, readdir, rm } from 'node:fs/promises';
5
3
  import { createServer } from 'node:net';
6
4
  import { join } from 'node:path';
7
- export async function frontendFiles(input) {
8
- const frontend = input.frontend;
9
- if (frontend === undefined)
10
- return [];
11
- if (frontend.source.kind === 'external')
12
- return frontendBuild.files(frontend, []);
13
- const output = join(input.projectDir, '.astrale', 'frontend-build');
14
- await rm(output, { recursive: true, force: true });
15
- await run(viteCommand(input.projectDir), [
16
- 'build',
17
- '--outDir',
18
- output,
19
- '--emptyOutDir',
20
- ...(frontend.source.config === undefined ? [] : ['--config', frontend.source.config]),
21
- ], input.projectDir, input.onLog);
22
- return frontendBuild.files(frontend, await readFiles(output));
23
- }
24
5
  export async function startFrontend(input) {
25
6
  const frontend = input.frontend;
26
7
  if (frontend === undefined || frontend.source.kind !== 'vite')
@@ -52,23 +33,32 @@ export async function startFrontend(input) {
52
33
  await ready(url, child, output);
53
34
  }
54
35
  catch (cause) {
55
- await stop(child);
36
+ await stopChild(child);
56
37
  throw cause;
57
38
  }
58
- return Object.freeze({ url, stop: () => stop(child) });
59
- }
60
- async function readFiles(root, current = '') {
61
- const entries = await readdir(join(root, current), { withFileTypes: true });
62
- const files = [];
63
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
64
- const path = current === '' ? entry.name : `${current}/${entry.name}`;
65
- if (entry.isDirectory())
66
- files.push(...(await readFiles(root, path)));
67
- else if (entry.isFile()) {
68
- files.push({ path, content: new Uint8Array(await readFile(join(root, path))) });
69
- }
70
- }
71
- return files;
39
+ let stopping = false;
40
+ const finished = child.exitCode === null
41
+ ? new Promise((resolve, reject) => {
42
+ child.once('exit', (code, signal) => {
43
+ if (stopping)
44
+ resolve();
45
+ else {
46
+ reject(new Error(`Vite stopped unexpectedly (code ${code ?? 'null'}, signal ${signal ?? 'none'}).\n${output.join('\n')}`));
47
+ }
48
+ });
49
+ child.once('error', reject);
50
+ })
51
+ : Promise.reject(new Error(`Vite stopped unexpectedly (code ${child.exitCode}).`));
52
+ return Object.freeze({
53
+ url,
54
+ finished,
55
+ async stop() {
56
+ if (stopping)
57
+ return finished.catch(() => undefined);
58
+ stopping = true;
59
+ await stopChild(child);
60
+ },
61
+ });
72
62
  }
73
63
  function viteCommand(projectDir) {
74
64
  const command = join(projectDir, 'node_modules', '.bin', 'vite');
@@ -76,25 +66,6 @@ function viteCommand(projectDir) {
76
66
  throw new TypeError('Vite frontend requires a project-local vite binary.');
77
67
  return command;
78
68
  }
79
- function run(command, args, cwd, onLog) {
80
- return new Promise((resolve, reject) => {
81
- const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
82
- let output = '';
83
- const collect = (buffer) => {
84
- const value = buffer.toString();
85
- output += value;
86
- for (const line of value.split('\n'))
87
- if (line.trim() !== '')
88
- onLog?.(line);
89
- };
90
- child.stdout?.on('data', collect);
91
- child.stderr?.on('data', collect);
92
- child.on('error', reject);
93
- child.on('exit', (code) => code === 0
94
- ? resolve()
95
- : reject(new Error(`Vite failed (code ${code ?? 1}):\n${output.slice(-1500)}`)));
96
- });
97
- }
98
69
  function ephemeralPort() {
99
70
  return new Promise((resolve, reject) => {
100
71
  const server = createServer();
@@ -123,11 +94,16 @@ async function ready(url, child, output) {
123
94
  }
124
95
  throw new Error(`Vite did not become ready at ${url}.`);
125
96
  }
126
- function stop(child) {
127
- if (child.exitCode !== null || child.killed)
97
+ function stopChild(child) {
98
+ if (child.exitCode !== null)
128
99
  return Promise.resolve();
129
100
  return new Promise((resolve) => {
130
101
  child.once('exit', () => resolve());
131
102
  child.kill('SIGTERM');
103
+ setTimeout(() => {
104
+ if (child.exitCode === null)
105
+ child.kill('SIGKILL');
106
+ resolve();
107
+ }, 4_000).unref?.();
132
108
  });
133
109
  }
@@ -0,0 +1,2 @@
1
+ /** Admit one externally owned public origin without inferring lifecycle ownership. */
2
+ export declare function admitHost(input: string): string;
@@ -0,0 +1,46 @@
1
+ /** Admit one externally owned public origin without inferring lifecycle ownership. */
2
+ export function admitHost(input) {
3
+ let url;
4
+ try {
5
+ url = new URL(input);
6
+ }
7
+ catch (cause) {
8
+ throw new TypeError('Cloudflare development host must be one public HTTP(S) origin.', { cause });
9
+ }
10
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
11
+ url.username !== '' ||
12
+ url.password !== '' ||
13
+ url.pathname !== '/' ||
14
+ url.search !== '' ||
15
+ url.hash !== '' ||
16
+ privateHostname(url.hostname)) {
17
+ throw new TypeError('Cloudflare development host must be one public HTTP(S) origin.');
18
+ }
19
+ return url.origin;
20
+ }
21
+ function privateHostname(input) {
22
+ const host = input
23
+ .toLowerCase()
24
+ .replace(/^\[|\]$/gu, '')
25
+ .replace(/\.$/u, '');
26
+ if (host === 'localhost' || host.endsWith('.localhost'))
27
+ return true;
28
+ if (isIP(host) === 4) {
29
+ const parts = host.split('.').map(Number);
30
+ return (parts[0] === 0 ||
31
+ parts[0] === 10 ||
32
+ parts[0] === 127 ||
33
+ (parts[0] === 169 && parts[1] === 254) ||
34
+ (parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31) ||
35
+ (parts[0] === 192 && parts[1] === 168));
36
+ }
37
+ if (isIP(host) !== 6)
38
+ return false;
39
+ return (host === '::' ||
40
+ host === '::1' ||
41
+ host.startsWith('fc') ||
42
+ host.startsWith('fd') ||
43
+ /^fe[89ab]/u.test(host) ||
44
+ /^::ffff:(?:0|10|127|169\.254|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./u.test(host));
45
+ }
46
+ import { isIP } from 'node:net';
@@ -0,0 +1 @@
1
+ export declare function allocatePort(host?: string): Promise<number>;
@@ -0,0 +1,12 @@
1
+ import { createServer } from 'node:net';
2
+ export function allocatePort(host = '127.0.0.1') {
3
+ return new Promise((resolve, reject) => {
4
+ const server = createServer();
5
+ server.once('error', reject);
6
+ server.listen(0, host, () => {
7
+ const address = server.address();
8
+ const port = typeof address === 'object' && address !== null ? address.port : 0;
9
+ server.close((cause) => (cause === undefined ? resolve(port) : reject(cause)));
10
+ });
11
+ });
12
+ }
@@ -0,0 +1,14 @@
1
+ import type { Release } from '@astrale-os/sdk/deployment/release';
2
+ import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
3
+ import { resolvedHttpsFetch } from './resolved-fetch.js';
4
+ interface ReadinessDependencies {
5
+ readonly verify: typeof verifyReadiness;
6
+ readonly resolvedFetch: typeof resolvedHttpsFetch;
7
+ }
8
+ /** Verify the exact Release over both public identity and its loopback transport. */
9
+ export declare function verifyDevelopmentRelease(input: {
10
+ readonly release: Release;
11
+ readonly localUrl: string;
12
+ readonly signal: AbortSignal;
13
+ }, dependencies?: ReadinessDependencies): Promise<void>;
14
+ export {};
@@ -0,0 +1,71 @@
1
+ import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
2
+ import { resolvedHttpsFetch } from './resolved-fetch.js';
3
+ const defaults = Object.freeze({
4
+ verify: verifyReadiness,
5
+ resolvedFetch: resolvedHttpsFetch,
6
+ });
7
+ /** Verify the exact Release over both public identity and its loopback transport. */
8
+ export async function verifyDevelopmentRelease(input, dependencies = defaults) {
9
+ const sameOrigin = new URL(input.release.addressing.issuer).origin === new URL(input.localUrl).origin;
10
+ if (!sameOrigin) {
11
+ await retry(() => dependencies.verify(input.release, {
12
+ signal: attemptSignal(input.signal),
13
+ fetch: (url, init) => {
14
+ const requested = new URL(url);
15
+ const local = new URL(input.localUrl);
16
+ local.pathname = requested.pathname;
17
+ local.search = requested.search;
18
+ return fetch(local.href, init);
19
+ },
20
+ }), input.signal);
21
+ }
22
+ const publicHost = new URL(input.release.addressing.issuer).hostname;
23
+ const publicFetch = publicHost.endsWith('.trycloudflare.com')
24
+ ? dependencies.resolvedFetch(input.release.addressing.issuer)
25
+ : undefined;
26
+ await retry(() => dependencies.verify(input.release, {
27
+ signal: attemptSignal(input.signal),
28
+ ...(publicFetch === undefined ? {} : { fetch: publicFetch }),
29
+ }), input.signal);
30
+ }
31
+ async function retry(operation, signal) {
32
+ const deadline = Date.now() + 90_000;
33
+ let last;
34
+ while (Date.now() < deadline) {
35
+ throwIfAborted(signal);
36
+ try {
37
+ await operation();
38
+ return;
39
+ }
40
+ catch (cause) {
41
+ throwIfAborted(signal);
42
+ last = cause;
43
+ await pause(signal);
44
+ }
45
+ }
46
+ throw last instanceof Error
47
+ ? last
48
+ : new Error('Cloudflare development Release did not become ready.');
49
+ }
50
+ function attemptSignal(signal) {
51
+ return AbortSignal.any([signal, AbortSignal.timeout(5_000)]);
52
+ }
53
+ function pause(signal) {
54
+ return new Promise((resolve, reject) => {
55
+ const timer = setTimeout(done, 250);
56
+ const abort = () => done(signal.reason instanceof Error ? signal.reason : new Error('Development interrupted.'));
57
+ signal.addEventListener('abort', abort, { once: true });
58
+ function done(cause) {
59
+ clearTimeout(timer);
60
+ signal.removeEventListener('abort', abort);
61
+ if (cause === undefined)
62
+ resolve();
63
+ else
64
+ reject(cause);
65
+ }
66
+ });
67
+ }
68
+ function throwIfAborted(signal) {
69
+ if (signal.aborted)
70
+ throw signal.reason instanceof Error ? signal.reason : new Error('Development interrupted.');
71
+ }
@@ -0,0 +1,18 @@
1
+ import type { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
2
+ import { resolve4 } from 'node:dns/promises';
3
+ type ReadinessOptions = NonNullable<Parameters<typeof verifyReadiness>[1]>;
4
+ type ReadinessFetch = NonNullable<ReadinessOptions['fetch']>;
5
+ type RequestAddress = typeof requestAddress;
6
+ export interface ResolvedFetchDependencies {
7
+ readonly resolve: typeof resolve4;
8
+ readonly request: RequestAddress;
9
+ }
10
+ /**
11
+ * Fetch one HTTPS origin through authoritative A records instead of the host
12
+ * process' getaddrinfo cache. Quick Tunnel hostnames can be negatively cached
13
+ * when their URL is observed milliseconds before DNS propagation; retrying
14
+ * global fetch in that process then remains stuck even after the record exists.
15
+ */
16
+ export declare function resolvedHttpsFetch(origin: string, dependencies?: ResolvedFetchDependencies): ReadinessFetch;
17
+ declare function requestAddress(url: URL, address: string, init: Parameters<ReadinessFetch>[1]): Promise<Response>;
18
+ export {};
@@ -0,0 +1,78 @@
1
+ import { resolve4 } from 'node:dns/promises';
2
+ import { request as httpsRequest } from 'node:https';
3
+ import { Readable } from 'node:stream';
4
+ const defaults = Object.freeze({
5
+ resolve: resolve4,
6
+ request: requestAddress,
7
+ });
8
+ /**
9
+ * Fetch one HTTPS origin through authoritative A records instead of the host
10
+ * process' getaddrinfo cache. Quick Tunnel hostnames can be negatively cached
11
+ * when their URL is observed milliseconds before DNS propagation; retrying
12
+ * global fetch in that process then remains stuck even after the record exists.
13
+ */
14
+ export function resolvedHttpsFetch(origin, dependencies = defaults) {
15
+ const expected = new URL(origin);
16
+ if (expected.protocol !== 'https:') {
17
+ throw new TypeError('Resolved development fetch requires an HTTPS origin.');
18
+ }
19
+ let addresses;
20
+ let cursor = 0;
21
+ return async (input, init) => {
22
+ const url = new URL(input);
23
+ if (url.origin !== expected.origin)
24
+ return fetch(input, init);
25
+ const resolved = addresses ?? (await dependencies.resolve(expected.hostname));
26
+ if (resolved.length === 0)
27
+ throw new Error(`No IPv4 address resolved for ${expected.hostname}.`);
28
+ addresses = resolved;
29
+ let last;
30
+ for (let attempt = 0; attempt < resolved.length; attempt += 1) {
31
+ const address = resolved[(cursor + attempt) % resolved.length];
32
+ try {
33
+ const response = await dependencies.request(url, address, init);
34
+ cursor = (cursor + attempt + 1) % resolved.length;
35
+ return response;
36
+ }
37
+ catch (cause) {
38
+ if (init?.signal?.aborted)
39
+ throw cause;
40
+ last = cause;
41
+ }
42
+ }
43
+ throw last;
44
+ };
45
+ }
46
+ function requestAddress(url, address, init) {
47
+ const headers = new Headers();
48
+ headers.set('host', url.host);
49
+ headers.set('accept-encoding', 'identity');
50
+ return new Promise((resolve, reject) => {
51
+ const request = httpsRequest({
52
+ protocol: 'https:',
53
+ hostname: address,
54
+ port: url.port === '' ? 443 : Number(url.port),
55
+ path: `${url.pathname}${url.search}`,
56
+ method: 'GET',
57
+ headers: Object.fromEntries(headers.entries()),
58
+ servername: url.hostname,
59
+ signal: init?.signal ?? undefined,
60
+ }, (incoming) => {
61
+ const responseHeaders = new Headers();
62
+ for (const [name, value] of Object.entries(incoming.headers)) {
63
+ if (Array.isArray(value))
64
+ for (const item of value)
65
+ responseHeaders.append(name, item);
66
+ else if (value !== undefined)
67
+ responseHeaders.set(name, value);
68
+ }
69
+ resolve(new Response(Readable.toWeb(incoming), {
70
+ status: incoming.statusCode ?? 500,
71
+ statusText: incoming.statusMessage,
72
+ headers: responseHeaders,
73
+ }));
74
+ });
75
+ request.once('error', reject);
76
+ request.end();
77
+ });
78
+ }
@@ -0,0 +1,2 @@
1
+ /** Admit the local Worker origin printed by Wrangler and ignore its inspector endpoints. */
2
+ export declare function parseWorkerReadyUrl(text: string, port: number): string | undefined;
@@ -0,0 +1,25 @@
1
+ const READY_RE = /(?:Ready on|Listening on|⎔.*?at)\s+(https?:\/\/[^\s]+)/iu;
2
+ /** Admit the local Worker origin printed by Wrangler and ignore its inspector endpoints. */
3
+ export function parseWorkerReadyUrl(text, port) {
4
+ const ready = READY_RE.exec(text);
5
+ if (ready !== null)
6
+ return normalizeLocal(ready[1], port);
7
+ const localhostOnPort = new RegExp(`(https?://(?:localhost|127\\.0\\.0\\.1):${port})(?![0-9])`, 'iu');
8
+ const match = localhostOnPort.exec(text);
9
+ return match === null ? undefined : normalizeLocal(match[1], port);
10
+ }
11
+ function normalizeLocal(input, port) {
12
+ try {
13
+ const url = new URL(input);
14
+ if (url.hostname === '0.0.0.0')
15
+ url.hostname = '127.0.0.1';
16
+ if (url.hostname === 'localhost')
17
+ url.hostname = '127.0.0.1';
18
+ if (url.port === '')
19
+ url.port = String(port);
20
+ return url.origin;
21
+ }
22
+ catch {
23
+ return `http://127.0.0.1:${port}`;
24
+ }
25
+ }
@@ -1,8 +1,9 @@
1
- export interface DevHandle {
2
- url: string;
1
+ export interface WorkerHandle {
2
+ readonly url: string;
3
+ readonly finished: Promise<void>;
3
4
  stop(): Promise<void>;
4
5
  }
5
- export declare function runWranglerDev(args: {
6
+ export declare function startWorker(args: {
6
7
  projectDir: string;
7
8
  configPath: string;
8
9
  port: number;
@@ -12,6 +13,5 @@ export declare function runWranglerDev(args: {
12
13
  /** Pin ordinary local requests to one stable issuer after the final port is selected. */
13
14
  readonly pinLocalOrigin?: boolean;
14
15
  readonly secrets: Readonly<Record<string, string>>;
15
- readonly onReload: () => void;
16
16
  readonly onLog?: (line: string) => void;
17
- }): Promise<DevHandle>;
17
+ }): Promise<WorkerHandle>;
@@ -1,10 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createServer } from 'node:net';
3
- import { parseDevReadyUrl } from '../parse-output.js';
4
- import { stopChild, wranglerCommand, wranglerProcessEnv } from './process.js';
5
- import { createSecretEnvironment, secretRedactor } from './secrets.js';
6
- import { runWranglerTypes } from './type-generation.js';
7
- export async function runWranglerDev(args) {
3
+ import { stopChild, wranglerCommand, wranglerProcessEnv } from '../wrangler/process.js';
4
+ import { createSecretEnvironment, secretRedactor } from '../wrangler/secrets.js';
5
+ import { runWranglerTypes } from '../wrangler/type-generation.js';
6
+ import { parseWorkerReadyUrl } from './worker-output.js';
7
+ export async function startWorker(args) {
8
8
  await runWranglerTypes(args);
9
9
  const { cmd, prefix } = wranglerCommand(args.projectDir);
10
10
  const bindHost = args.ip ?? '127.0.0.1';
@@ -60,15 +60,12 @@ export async function runWranglerDev(args) {
60
60
  buildErrorTail = (buildErrorTail + text).slice(-2000);
61
61
  }
62
62
  if (!resolved) {
63
- const ready = parseDevReadyUrl(text, args.port);
63
+ const ready = parseWorkerReadyUrl(text, args.port);
64
64
  if (ready) {
65
65
  resolved = true;
66
66
  resolve(ready);
67
67
  }
68
68
  }
69
- else if (/Reloading|Detected changes|Updated/i.test(text)) {
70
- args.onReload();
71
- }
72
69
  };
73
70
  child.stdout?.on('data', onData);
74
71
  child.stderr?.on('data', onData);
@@ -88,11 +85,10 @@ export async function runWranglerDev(args) {
88
85
  if (!resolved && child.exitCode === null) {
89
86
  resolved = true;
90
87
  if (buildErrorTail) {
91
- void stopChild(child);
92
88
  reject(new Error(`worker build failed — the dev server would hang silently.\n${buildErrorTail}`));
93
89
  }
94
90
  else {
95
- resolve(fallbackUrl);
91
+ reject(new Error(`wrangler dev did not report local readiness at ${fallbackUrl}.\n${outputTail}`));
96
92
  }
97
93
  }
98
94
  }, 15_000).unref?.();
@@ -103,9 +99,24 @@ export async function runWranglerDev(args) {
103
99
  await secretEnvironment?.remove();
104
100
  throw cause;
105
101
  }
106
- return {
102
+ let stopping = false;
103
+ const finished = new Promise((resolve, reject) => {
104
+ child.once('exit', (code, signal) => {
105
+ if (stopping)
106
+ resolve();
107
+ else {
108
+ reject(new Error(`wrangler dev stopped unexpectedly (code ${code ?? 'null'}, signal ${signal ?? 'none'}).\n${outputTail}`));
109
+ }
110
+ });
111
+ child.once('error', reject);
112
+ });
113
+ return Object.freeze({
107
114
  url: args.pinLocalOrigin ? fallbackUrl : url,
115
+ finished,
108
116
  async stop() {
117
+ if (stopping)
118
+ return finished;
119
+ stopping = true;
109
120
  try {
110
121
  await stopChild(child);
111
122
  }
@@ -113,7 +124,7 @@ export async function runWranglerDev(args) {
113
124
  await secretEnvironment?.remove();
114
125
  }
115
126
  },
116
- };
127
+ });
117
128
  }
118
129
  function portFree(port, host) {
119
130
  return new Promise((resolve) => {
@@ -1,5 +1,6 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { existsSync } from 'node:fs';
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { link, mkdir, open, readFile, unlink } from 'node:fs/promises';
3
4
  import { dirname, isAbsolute, join, resolve } from 'node:path';
4
5
  import { SIGNING_IDENTITY_BINDING } from './binding.js';
5
6
  import { admitSigningIdentity, createSigningIdentity } from './material.js';
@@ -41,10 +42,10 @@ export async function localSigningIdentity(projectDir, declaration, origin) {
41
42
  }
42
43
  async function generateIdentity(path) {
43
44
  const value = await createSigningIdentity();
44
- await mkdir(dirname(path), { recursive: true });
45
- await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
46
- process.stderr.write(`\x1b[33m!\x1b[0m new signing identity generated at .astrale/identity.json (kid ${value.kid}).\n` +
47
- ` Retain it in managed secret storage before deploying; remote deploy fails closed without an explicit identity file.\n`);
45
+ if (await installIdentity(path, value)) {
46
+ process.stderr.write(`\x1b[33m!\x1b[0m new signing identity generated at .astrale/identity.json (kid ${value.kid}).\n` +
47
+ ` Retain it in managed secret storage before deploying; remote deploy fails closed without an explicit identity file.\n`);
48
+ }
48
49
  }
49
50
  async function migrateLegacyIdentity(legacy, destination, origin) {
50
51
  const source = await readFile(legacy, 'utf8');
@@ -59,9 +60,37 @@ async function migrateLegacyIdentity(legacy, destination, origin) {
59
60
  throw invalidIdentity(legacy, cause);
60
61
  }
61
62
  const value = await admitSigningIdentity(decoded, legacy, `${origin}-key`);
62
- await mkdir(dirname(destination), { recursive: true });
63
- await writeFile(destination, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
64
- process.stderr.write(`\x1b[33m!\x1b[0m migrated .astrale/identity.ts to secret-backed .astrale/identity.json (kid ${value.kid}).\n`);
63
+ if (await installIdentity(destination, value)) {
64
+ process.stderr.write(`\x1b[33m!\x1b[0m migrated .astrale/identity.ts to secret-backed .astrale/identity.json (kid ${value.kid}).\n`);
65
+ }
66
+ }
67
+ /** Publish a complete owner-only identity once; concurrent environments reuse the winner. */
68
+ async function installIdentity(path, value) {
69
+ await mkdir(dirname(path), { recursive: true });
70
+ const staging = `${path}.${process.pid}.${randomUUID()}.next`;
71
+ const file = await open(staging, 'wx', 0o600);
72
+ try {
73
+ await file.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
74
+ await file.sync();
75
+ }
76
+ catch (cause) {
77
+ await file.close();
78
+ await unlink(staging).catch(() => undefined);
79
+ throw cause;
80
+ }
81
+ await file.close();
82
+ try {
83
+ await link(staging, path);
84
+ return true;
85
+ }
86
+ catch (cause) {
87
+ if (!(cause instanceof Error && 'code' in cause && cause.code === 'EEXIST'))
88
+ throw cause;
89
+ return false;
90
+ }
91
+ finally {
92
+ await unlink(staging).catch(() => undefined);
93
+ }
65
94
  }
66
95
  async function readSource(path, declaration) {
67
96
  let source;
package/dist/index.d.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  * prod: { route: 'crm.acme.dev', secrets: '.env.prod', signingIdentity: '.astrale/identity.json' },
10
10
  * })
11
11
  *
12
- * `dev` runs `wrangler dev` locally; an env with no `route` ships to
12
+ * `dev` runs the Worker and optional Vite frontend on allocated local ports;
13
+ * an env with no `route` ships to
13
14
  * `*.workers.dev`; an env with a `route` ships to that custom domain.
14
15
  *
15
16
  * Prefer a managed deploy (no Cloudflare account)? Use
package/dist/index.js CHANGED
@@ -9,7 +9,8 @@
9
9
  * prod: { route: 'crm.acme.dev', secrets: '.env.prod', signingIdentity: '.astrale/identity.json' },
10
10
  * })
11
11
  *
12
- * `dev` runs `wrangler dev` locally; an env with no `route` ships to
12
+ * `dev` runs the Worker and optional Vite frontend on allocated local ports;
13
+ * an env with no `route` ships to
13
14
  * `*.workers.dev`; an env with a `route` ships to that custom domain.
14
15
  *
15
16
  * Prefer a managed deploy (no Cloudflare account)? Use
package/dist/params.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * is `Record<string, CloudflareParams>`.
7
7
  *
8
8
  * Three deployment shapes fall out of which fields are set:
9
- * • dev (local) — `watch()` runs `wrangler dev`; URL = http://localhost:<port>
9
+ * • dev (local) — `develop()` runs `wrangler dev`; URL = an allocated loopback port
10
10
  * • dev-remote — `deploy()` with no `route`; URL = https://<name>.<sub>.workers.dev
11
11
  * • remote (custom) — `deploy()` with `route`; URL = https://<route>
12
12
  */
@@ -29,11 +29,11 @@ export interface CloudflareParams {
29
29
  /**
30
30
  * Private Ed25519 JWK file retained outside source control. Remote deploys
31
31
  * require this explicit source so a fresh runner cannot silently rotate an
32
- * installed Domain identity. Local watch generates `.astrale/identity.json`
32
+ * installed Domain identity. Local development generates `.astrale/identity.json`
33
33
  * once when this field is omitted.
34
34
  */
35
35
  readonly signingIdentity?: string;
36
- /** Local dev port for `wrangler dev`. Default 8787. */
36
+ /** Strict local development port. Omit to allocate an available loopback port. */
37
37
  readonly port?: number;
38
38
  /**
39
39
  * Public URL the dev server is reached through (tunnel / sandbox preview /
@@ -1,9 +1,7 @@
1
1
  import { readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { runCapture, wranglerCommand } from './process.js';
4
- import { runWranglerTypes } from './type-generation.js';
5
4
  export async function runWranglerBundle(args) {
6
- await runWranglerTypes(args);
7
5
  const { cmd, prefix } = wranglerCommand(args.projectDir);
8
6
  const { code, out } = await runCapture(cmd, [
9
7
  ...prefix,
@@ -1,4 +1,4 @@
1
- import { parseDeployUrl, parseDeployVersionId } from '../parse-output.js';
1
+ import { parseDeployUrl, parseDeployVersionId } from '../deploy/output.js';
2
2
  import { runCapture, wranglerCommand } from './process.js';
3
3
  import { secretRedactor, withSecretsFile } from './secrets.js';
4
4
  import { runWranglerTypes } from './type-generation.js';
@@ -0,0 +1,5 @@
1
+ export { runWranglerBundle } from './bundle.js';
2
+ export { runWranglerDeploy, WranglerDeployError, type DeployArgs } from './deploy.js';
3
+ export { wranglerProcessEnv } from './process.js';
4
+ export { admitWranglerDeploymentStatus, runWranglerDeploymentStatus, type WranglerDeploymentStatus, } from './status.js';
5
+ export { runWranglerTypes } from './type-generation.js';