@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.
- package/CHANGELOG.md +14 -0
- package/README.md +4 -0
- package/dist/adapter.d.ts +2 -4
- package/dist/adapter.js +5 -106
- package/dist/build/artifact.js +7 -5
- package/dist/build/{frontend-v1.d.ts → frontend/files.d.ts} +0 -9
- package/dist/build/frontend/files.js +60 -0
- package/dist/build/index.d.ts +2 -2
- package/dist/build/index.js +2 -2
- package/dist/build/worker/execution.prepare.js +1 -1
- package/dist/deploy/deploy.d.ts +2 -1
- package/dist/deploy/deploy.js +1 -1
- package/dist/deploy/output.d.ts +2 -0
- package/dist/deploy/output.js +10 -0
- package/dist/development/develop.d.ts +15 -0
- package/dist/development/develop.js +267 -0
- package/dist/development/frontend.d.ts +11 -0
- package/dist/{build/frontend-v1.js → development/frontend.js} +31 -55
- package/dist/development/host.d.ts +2 -0
- package/dist/development/host.js +46 -0
- package/dist/development/port.d.ts +1 -0
- package/dist/development/port.js +12 -0
- package/dist/development/readiness.d.ts +14 -0
- package/dist/development/readiness.js +71 -0
- package/dist/development/resolved-fetch.d.ts +18 -0
- package/dist/development/resolved-fetch.js +78 -0
- package/dist/development/worker-output.d.ts +2 -0
- package/dist/development/worker-output.js +25 -0
- package/dist/{build/wrangler/development.d.ts → development/worker.d.ts} +5 -5
- package/dist/{build/wrangler/development.js → development/worker.js} +24 -13
- package/dist/identity/source.js +37 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/params.d.ts +3 -3
- package/dist/{build/wrangler → wrangler}/bundle.js +0 -2
- package/dist/{build/wrangler → wrangler}/deploy.js +1 -1
- package/dist/wrangler/index.d.ts +5 -0
- package/dist/wrangler/index.js +5 -0
- package/package.json +2 -2
- package/template/astrale.config.ts +6 -6
- package/dist/build/parse-output.d.ts +0 -26
- package/dist/build/parse-output.js +0 -57
- package/dist/build/wrangler-cli.d.ts +0 -6
- package/dist/build/wrangler-cli.js +0 -6
- /package/dist/{build/wrangler → wrangler}/bundle.d.ts +0 -0
- /package/dist/{build/wrangler → wrangler}/deploy.d.ts +0 -0
- /package/dist/{build/wrangler → wrangler}/process.d.ts +0 -0
- /package/dist/{build/wrangler → wrangler}/process.js +0 -0
- /package/dist/{build/wrangler → wrangler}/secrets.d.ts +0 -0
- /package/dist/{build/wrangler → wrangler}/secrets.js +0 -0
- /package/dist/{build/wrangler → wrangler}/status.d.ts +0 -0
- /package/dist/{build/wrangler → wrangler}/status.js +0 -0
- /package/dist/{build/wrangler → wrangler}/type-generation.d.ts +0 -0
- /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
|
|
36
|
+
await stopChild(child);
|
|
56
37
|
throw cause;
|
|
57
38
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return
|
|
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
|
|
127
|
-
if (child.exitCode !== null
|
|
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,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,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
|
|
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
|
|
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<
|
|
17
|
+
}): Promise<WorkerHandle>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:net';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
export async function
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
package/dist/identity/source.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile,
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
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) — `
|
|
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
|
|
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
|
-
/**
|
|
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 '../
|
|
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';
|