@astrale-os/adapter-cloudflare 0.5.0-beta.9 → 0.5.0-beta.91
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 +1097 -0
- package/README.md +20 -2
- package/dist/adapter.d.ts +15 -0
- package/dist/adapter.js +76 -0
- package/dist/build/artifact.d.ts +7 -0
- package/dist/build/artifact.js +165 -0
- package/dist/build/bundle/admit.d.ts +1 -0
- package/dist/build/bundle/admit.js +9 -0
- package/dist/build/closure.d.ts +2 -0
- package/dist/build/closure.js +22 -0
- package/dist/build/codegen/execution.d.ts +3 -0
- package/dist/build/codegen/execution.js +2 -0
- package/dist/build/codegen/merge.d.ts +0 -1
- package/dist/build/codegen/merge.js +0 -1
- package/dist/build/codegen/wrangler.d.ts +13 -1
- package/dist/build/codegen/wrangler.js +35 -18
- package/dist/build/configuration/addressing.d.ts +2 -0
- package/dist/build/configuration/addressing.js +11 -0
- package/dist/build/configuration/invocation-timeout.d.ts +2 -0
- package/dist/build/configuration/invocation-timeout.js +9 -0
- package/dist/build/configuration/request-limit.d.ts +2 -0
- package/dist/build/configuration/request-limit.js +9 -0
- package/dist/build/configuration/router.d.ts +7 -0
- package/dist/build/configuration/router.js +11 -0
- package/dist/build/configuration/worker-name.d.ts +2 -0
- package/dist/build/configuration/worker-name.js +18 -0
- package/dist/build/configuration.d.ts +5 -0
- package/dist/build/configuration.js +5 -0
- package/dist/build/direct.d.ts +4 -0
- package/dist/build/direct.js +93 -0
- package/dist/build/frontend/files.d.ts +7 -0
- package/dist/build/frontend/files.js +60 -0
- package/dist/build/frontend/routes.d.ts +2 -0
- package/dist/build/frontend/routes.js +11 -0
- package/dist/build/index.d.ts +8 -16
- package/dist/build/index.js +7 -16
- package/dist/build/materialize.d.ts +4 -0
- package/dist/build/materialize.js +10 -0
- package/dist/build/metafile.d.ts +5 -0
- package/dist/build/metafile.js +14 -0
- package/dist/build/qualification.d.ts +5 -0
- package/dist/build/qualification.js +7 -0
- package/dist/build/roots.d.ts +2 -0
- package/dist/build/roots.js +34 -0
- package/dist/build/worker/artifact.d.ts +40 -0
- package/dist/build/worker/artifact.js +1 -0
- package/dist/build/worker/artifact.load.d.ts +3 -0
- package/dist/build/worker/artifact.load.js +308 -0
- package/dist/build/worker/artifact.manifest.d.ts +48 -0
- package/dist/build/worker/artifact.manifest.js +87 -0
- package/dist/build/worker/artifact.paths.d.ts +4 -0
- package/dist/build/worker/artifact.paths.js +25 -0
- package/dist/build/worker/codegen/entry.d.ts +2 -0
- package/dist/build/worker/codegen/entry.js +28 -0
- package/dist/build/worker/codegen/service.d.ts +18 -0
- package/dist/build/worker/codegen/service.js +53 -0
- package/dist/build/worker/execution.prepare.d.ts +18 -0
- package/dist/build/worker/execution.prepare.js +112 -0
- package/dist/build/worker/index.d.ts +2 -0
- package/dist/build/worker/index.js +1 -0
- package/dist/build/worker/modules.d.ts +11 -0
- package/dist/build/worker/modules.js +26 -0
- package/dist/build/worker/runtime.d.ts +10 -0
- package/dist/build/worker/runtime.js +35 -0
- package/dist/build/worker/specifier.d.ts +1 -0
- package/dist/build/worker/specifier.js +5 -0
- package/dist/cloudflare.d.ts +1 -34
- package/dist/cloudflare.js +1 -205
- package/dist/deploy/deploy.d.ts +16 -0
- package/dist/deploy/deploy.js +204 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/index.js +1 -0
- 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 +270 -0
- package/dist/development/frontend.d.ts +11 -0
- package/dist/development/frontend.js +109 -0
- 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 +17 -0
- package/dist/development/resolved-fetch.js +80 -0
- package/dist/development/worker-output.d.ts +2 -0
- package/dist/development/worker-output.js +25 -0
- package/dist/development/worker.d.ts +17 -0
- package/dist/development/worker.js +147 -0
- package/dist/identity/binding.d.ts +2 -0
- package/dist/identity/binding.js +2 -0
- package/dist/identity/index.d.ts +3 -0
- package/dist/identity/index.js +3 -0
- package/dist/identity/material.d.ts +7 -0
- package/dist/identity/material.js +42 -0
- package/dist/identity/secret.d.ts +3 -0
- package/dist/identity/secret.js +7 -0
- package/dist/identity/source.d.ts +26 -0
- package/dist/identity/source.js +127 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/params.d.ts +39 -7
- package/dist/params.js +0 -1
- package/dist/release/deployment.d.ts +44 -0
- package/dist/release/deployment.js +141 -0
- package/dist/release/index.d.ts +2 -0
- package/dist/release/index.js +2 -0
- package/dist/release/staged-worker-name.d.ts +3 -0
- package/dist/release/staged-worker-name.js +10 -0
- package/dist/worker/entry.d.ts +44 -0
- package/dist/worker/entry.js +184 -0
- package/dist/worker/fetch.d.ts +0 -1
- package/dist/worker/fetch.js +0 -1
- package/dist/worker/identity.d.ts +7 -0
- package/dist/worker/identity.js +23 -0
- package/dist/worker/index.d.ts +2 -61
- package/dist/worker/index.js +2 -212
- package/dist/wrangler/bundle.d.ts +10 -0
- package/dist/wrangler/bundle.js +27 -0
- package/dist/wrangler/deploy.d.ts +24 -0
- package/dist/wrangler/deploy.js +44 -0
- package/dist/wrangler/index.d.ts +5 -0
- package/dist/wrangler/index.js +5 -0
- package/dist/wrangler/process.d.ts +17 -0
- package/dist/wrangler/process.js +115 -0
- package/dist/wrangler/secrets.d.ts +6 -0
- package/dist/wrangler/secrets.js +58 -0
- package/dist/wrangler/status.d.ts +14 -0
- package/dist/wrangler/status.js +56 -0
- package/dist/wrangler/type-generation.d.ts +6 -0
- package/dist/wrangler/type-generation.js +19 -0
- package/package.json +34 -19
- package/template/astrale.config.ts +25 -17
- package/dist/build/codegen/identity.d.ts +0 -10
- package/dist/build/codegen/identity.d.ts.map +0 -1
- package/dist/build/codegen/identity.js +0 -32
- package/dist/build/codegen/identity.js.map +0 -1
- package/dist/build/codegen/merge.d.ts.map +0 -1
- package/dist/build/codegen/merge.js.map +0 -1
- package/dist/build/codegen/worker.d.ts +0 -12
- package/dist/build/codegen/worker.d.ts.map +0 -1
- package/dist/build/codegen/worker.js +0 -40
- package/dist/build/codegen/worker.js.map +0 -1
- package/dist/build/codegen/wrangler.d.ts.map +0 -1
- package/dist/build/codegen/wrangler.js.map +0 -1
- package/dist/build/frontend.d.ts +0 -26
- package/dist/build/frontend.d.ts.map +0 -1
- package/dist/build/frontend.js +0 -263
- package/dist/build/frontend.js.map +0 -1
- package/dist/build/index.d.ts.map +0 -1
- package/dist/build/index.js.map +0 -1
- package/dist/build/parse-output.d.ts +0 -27
- package/dist/build/parse-output.d.ts.map +0 -1
- package/dist/build/parse-output.js +0 -58
- package/dist/build/parse-output.js.map +0 -1
- package/dist/build/prepare.d.ts +0 -18
- package/dist/build/prepare.d.ts.map +0 -1
- package/dist/build/prepare.js +0 -117
- package/dist/build/prepare.js.map +0 -1
- package/dist/build/worker.d.ts +0 -15
- package/dist/build/worker.d.ts.map +0 -1
- package/dist/build/worker.js +0 -21
- package/dist/build/worker.js.map +0 -1
- package/dist/build/wrangler-cli.d.ts +0 -88
- package/dist/build/wrangler-cli.d.ts.map +0 -1
- package/dist/build/wrangler-cli.js +0 -283
- package/dist/build/wrangler-cli.js.map +0 -1
- package/dist/cloudflare.d.ts.map +0 -1
- package/dist/cloudflare.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/params.d.ts.map +0 -1
- package/dist/params.js.map +0 -1
- package/dist/worker/fetch.d.ts.map +0 -1
- package/dist/worker/fetch.js.map +0 -1
- package/dist/worker/index.d.ts.map +0 -1
- package/dist/worker/index.js.map +0 -1
- package/src/build/codegen/identity.ts +0 -40
- package/src/build/codegen/merge.ts +0 -92
- package/src/build/codegen/worker.ts +0 -53
- package/src/build/codegen/wrangler.ts +0 -127
- package/src/build/frontend.ts +0 -324
- package/src/build/index.ts +0 -29
- package/src/build/parse-output.ts +0 -60
- package/src/build/prepare.ts +0 -161
- package/src/build/worker.ts +0 -37
- package/src/build/wrangler-cli.ts +0 -372
- package/src/cloudflare.ts +0 -251
- package/src/index.ts +0 -21
- package/src/params.ts +0 -92
- package/src/worker/fetch.ts +0 -46
- package/src/worker/index.ts +0 -325
- package/src/worker/tsconfig.json +0 -10
|
@@ -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,17 @@
|
|
|
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 the stable TryCloudflare edge addresses.
|
|
12
|
+
* Fresh Quick Tunnel hostnames can remain absent from DNS after the connector
|
|
13
|
+
* is registered, while TLS SNI and Host already route through the stable edge.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolvedHttpsFetch(origin: string, dependencies?: ResolvedFetchDependencies): ReadinessFetch;
|
|
16
|
+
declare function requestAddress(url: URL, address: string, init: Parameters<ReadinessFetch>[1]): Promise<Response>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
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 the stable TryCloudflare edge addresses.
|
|
10
|
+
* Fresh Quick Tunnel hostnames can remain absent from DNS after the connector
|
|
11
|
+
* is registered, while TLS SNI and Host already route through the stable edge.
|
|
12
|
+
*/
|
|
13
|
+
export function resolvedHttpsFetch(origin, dependencies = defaults) {
|
|
14
|
+
const expected = new URL(origin);
|
|
15
|
+
if (expected.protocol !== 'https:') {
|
|
16
|
+
throw new TypeError('Resolved development fetch requires an HTTPS origin.');
|
|
17
|
+
}
|
|
18
|
+
const resolutionHost = expected.hostname.endsWith('.trycloudflare.com')
|
|
19
|
+
? 'trycloudflare.com'
|
|
20
|
+
: expected.hostname;
|
|
21
|
+
let addresses;
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
return async (input, init) => {
|
|
24
|
+
const url = new URL(input);
|
|
25
|
+
if (url.origin !== expected.origin)
|
|
26
|
+
return fetch(input, init);
|
|
27
|
+
const resolved = addresses ?? (await dependencies.resolve(resolutionHost));
|
|
28
|
+
if (resolved.length === 0)
|
|
29
|
+
throw new Error(`No IPv4 address resolved for ${expected.hostname}.`);
|
|
30
|
+
addresses = resolved;
|
|
31
|
+
let last;
|
|
32
|
+
for (let attempt = 0; attempt < resolved.length; attempt += 1) {
|
|
33
|
+
const address = resolved[(cursor + attempt) % resolved.length];
|
|
34
|
+
try {
|
|
35
|
+
const response = await dependencies.request(url, address, init);
|
|
36
|
+
cursor = (cursor + attempt + 1) % resolved.length;
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (init?.signal?.aborted)
|
|
41
|
+
throw cause;
|
|
42
|
+
last = cause;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw last;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function requestAddress(url, address, init) {
|
|
49
|
+
const headers = new Headers();
|
|
50
|
+
headers.set('host', url.host);
|
|
51
|
+
headers.set('accept-encoding', 'identity');
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const request = httpsRequest({
|
|
54
|
+
protocol: 'https:',
|
|
55
|
+
hostname: address,
|
|
56
|
+
port: url.port === '' ? 443 : Number(url.port),
|
|
57
|
+
path: `${url.pathname}${url.search}`,
|
|
58
|
+
method: 'GET',
|
|
59
|
+
headers: Object.fromEntries(headers.entries()),
|
|
60
|
+
servername: url.hostname,
|
|
61
|
+
signal: init?.signal ?? undefined,
|
|
62
|
+
}, (incoming) => {
|
|
63
|
+
const responseHeaders = new Headers();
|
|
64
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
65
|
+
if (Array.isArray(value))
|
|
66
|
+
for (const item of value)
|
|
67
|
+
responseHeaders.append(name, item);
|
|
68
|
+
else if (value !== undefined)
|
|
69
|
+
responseHeaders.set(name, value);
|
|
70
|
+
}
|
|
71
|
+
resolve(new Response(Readable.toWeb(incoming), {
|
|
72
|
+
status: incoming.statusCode ?? 500,
|
|
73
|
+
statusText: incoming.statusMessage,
|
|
74
|
+
headers: responseHeaders,
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
request.once('error', reject);
|
|
78
|
+
request.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface WorkerHandle {
|
|
2
|
+
readonly url: string;
|
|
3
|
+
readonly finished: Promise<void>;
|
|
4
|
+
stop(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function startWorker(args: {
|
|
7
|
+
projectDir: string;
|
|
8
|
+
configPath: string;
|
|
9
|
+
port: number;
|
|
10
|
+
remote: boolean;
|
|
11
|
+
readonly ip?: string;
|
|
12
|
+
readonly autoPickPort?: boolean;
|
|
13
|
+
/** Pin ordinary local requests to one stable issuer after the final port is selected. */
|
|
14
|
+
readonly pinLocalOrigin?: boolean;
|
|
15
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
16
|
+
readonly onLog?: (line: string) => void;
|
|
17
|
+
}): Promise<WorkerHandle>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
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
|
+
await runWranglerTypes(args);
|
|
9
|
+
const { cmd, prefix } = wranglerCommand(args.projectDir);
|
|
10
|
+
const bindHost = args.ip ?? '127.0.0.1';
|
|
11
|
+
if (args.autoPickPort && !(await portFree(args.port, bindHost))) {
|
|
12
|
+
const picked = await ephemeralPort(bindHost);
|
|
13
|
+
args.onLog?.(`port ${args.port} is taken — using ${picked} instead (pass --port to pin one)`);
|
|
14
|
+
args = { ...args, port: picked };
|
|
15
|
+
}
|
|
16
|
+
const secretEnvironment = await createSecretEnvironment(args.secrets);
|
|
17
|
+
const redact = secretRedactor(args.secrets);
|
|
18
|
+
const fallbackUrl = `http://localhost:${args.port}`;
|
|
19
|
+
const wranglerArgs = [
|
|
20
|
+
...prefix,
|
|
21
|
+
'dev',
|
|
22
|
+
'--config',
|
|
23
|
+
args.configPath,
|
|
24
|
+
'--port',
|
|
25
|
+
String(args.port),
|
|
26
|
+
'--local-protocol',
|
|
27
|
+
'http',
|
|
28
|
+
'--ip',
|
|
29
|
+
args.ip ?? '127.0.0.1',
|
|
30
|
+
...(args.pinLocalOrigin ? ['--var', `WORKER_URL:${fallbackUrl}`] : []),
|
|
31
|
+
...(secretEnvironment === undefined ? [] : ['--env-file', secretEnvironment.path]),
|
|
32
|
+
...(args.remote ? ['--remote'] : []),
|
|
33
|
+
];
|
|
34
|
+
let child;
|
|
35
|
+
try {
|
|
36
|
+
child = spawn(cmd, wranglerArgs, {
|
|
37
|
+
cwd: args.projectDir,
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
env: wranglerProcessEnv(args.projectDir),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
await secretEnvironment?.remove();
|
|
44
|
+
throw cause;
|
|
45
|
+
}
|
|
46
|
+
let resolved = false;
|
|
47
|
+
let buildErrorTail = '';
|
|
48
|
+
let outputTail = '';
|
|
49
|
+
let url;
|
|
50
|
+
try {
|
|
51
|
+
url = await new Promise((resolve, reject) => {
|
|
52
|
+
const onData = (buf) => {
|
|
53
|
+
const text = redact(buf.toString());
|
|
54
|
+
outputTail = (outputTail + text).slice(-3000);
|
|
55
|
+
for (const line of text.split('\n')) {
|
|
56
|
+
if (line.trim())
|
|
57
|
+
args.onLog?.(line);
|
|
58
|
+
}
|
|
59
|
+
if (/✘ \[ERROR\]|Build failed|Could not resolve "/.test(text)) {
|
|
60
|
+
buildErrorTail = (buildErrorTail + text).slice(-2000);
|
|
61
|
+
}
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
const ready = parseWorkerReadyUrl(text, args.port);
|
|
64
|
+
if (ready) {
|
|
65
|
+
resolved = true;
|
|
66
|
+
resolve(ready);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
child.stdout?.on('data', onData);
|
|
71
|
+
child.stderr?.on('data', onData);
|
|
72
|
+
child.on('exit', (code) => {
|
|
73
|
+
if (!resolved) {
|
|
74
|
+
resolved = true;
|
|
75
|
+
reject(new Error(`wrangler dev exited before becoming ready (code ${code ?? 'null'}).\n${outputTail}`));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
child.on('error', (error) => {
|
|
79
|
+
if (!resolved) {
|
|
80
|
+
resolved = true;
|
|
81
|
+
reject(error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if (!resolved && child.exitCode === null) {
|
|
86
|
+
resolved = true;
|
|
87
|
+
if (buildErrorTail) {
|
|
88
|
+
reject(new Error(`worker build failed — the dev server would hang silently.\n${buildErrorTail}`));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
reject(new Error(`wrangler dev did not report local readiness at ${fallbackUrl}.\n${outputTail}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, 15_000).unref?.();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
await stopChild(child);
|
|
99
|
+
await secretEnvironment?.remove();
|
|
100
|
+
throw cause;
|
|
101
|
+
}
|
|
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({
|
|
114
|
+
url: args.pinLocalOrigin ? fallbackUrl : url,
|
|
115
|
+
finished,
|
|
116
|
+
async stop() {
|
|
117
|
+
if (stopping)
|
|
118
|
+
return finished;
|
|
119
|
+
stopping = true;
|
|
120
|
+
try {
|
|
121
|
+
await stopChild(child);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await secretEnvironment?.remove();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function portFree(port, host) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const server = createServer();
|
|
132
|
+
server.once('error', () => resolve(false));
|
|
133
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
134
|
+
server.listen(port, host);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function ephemeralPort(host) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const server = createServer();
|
|
140
|
+
server.once('error', reject);
|
|
141
|
+
server.listen(0, host, () => {
|
|
142
|
+
const address = server.address();
|
|
143
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
144
|
+
server.close(() => resolve(port));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonWebKey } from '@astrale-os/sdk/auth';
|
|
2
|
+
export type SigningIdentityMaterial = JsonWebKey & {
|
|
3
|
+
readonly kid: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function createSigningIdentity(): Promise<SigningIdentityMaterial>;
|
|
6
|
+
/** Admit private Ed25519 material and its collision-resistant public identity. */
|
|
7
|
+
export declare function admitSigningIdentity(input: unknown, owner: string, legacyKeyId?: string): Promise<SigningIdentityMaterial>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK } from 'jose';
|
|
2
|
+
export async function createSigningIdentity() {
|
|
3
|
+
const { privateKey } = await generateKeyPair('EdDSA', { extractable: true });
|
|
4
|
+
const exported = await exportJWK(privateKey);
|
|
5
|
+
return admitSigningIdentity({
|
|
6
|
+
...exported,
|
|
7
|
+
alg: 'EdDSA',
|
|
8
|
+
kid: await calculateJwkThumbprint(exported),
|
|
9
|
+
}, 'generated identity');
|
|
10
|
+
}
|
|
11
|
+
/** Admit private Ed25519 material and its collision-resistant public identity. */
|
|
12
|
+
export async function admitSigningIdentity(input, owner, legacyKeyId) {
|
|
13
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
14
|
+
invalid(owner);
|
|
15
|
+
const value = input;
|
|
16
|
+
if (value.kty !== 'OKP' ||
|
|
17
|
+
value.crv !== 'Ed25519' ||
|
|
18
|
+
value.alg !== 'EdDSA' ||
|
|
19
|
+
typeof value.x !== 'string' ||
|
|
20
|
+
value.x.length === 0 ||
|
|
21
|
+
typeof value.d !== 'string' ||
|
|
22
|
+
value.d.length === 0) {
|
|
23
|
+
invalid(owner);
|
|
24
|
+
}
|
|
25
|
+
const thumbprint = await calculateJwkThumbprint(value);
|
|
26
|
+
const admitted = legacyKeyId !== undefined && (value.kid === undefined || value.kid === legacyKeyId)
|
|
27
|
+
? { ...value, kid: thumbprint }
|
|
28
|
+
: value;
|
|
29
|
+
if (admitted.kid !== thumbprint) {
|
|
30
|
+
throw new TypeError(`Cloudflare signing identity '${owner}' must use its JWK thumbprint as kid.`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await importJWK(admitted, 'EdDSA');
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
throw invalid(owner, cause);
|
|
37
|
+
}
|
|
38
|
+
return admitted;
|
|
39
|
+
}
|
|
40
|
+
function invalid(owner, cause) {
|
|
41
|
+
throw new TypeError(`Cloudflare signing identity '${owner}' is not a private Ed25519 JWK.`, cause === undefined ? undefined : { cause });
|
|
42
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { SigningIdentitySource } from './source.js';
|
|
2
|
+
/** Add the adapter-owned identity without allowing a second secret authority. */
|
|
3
|
+
export declare function signingIdentitySecrets(secrets: Readonly<Record<string, string>>, identity: SigningIdentitySource): Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Add the adapter-owned identity without allowing a second secret authority. */
|
|
2
|
+
export function signingIdentitySecrets(secrets, identity) {
|
|
3
|
+
if (Object.hasOwn(secrets, identity.binding)) {
|
|
4
|
+
throw new TypeError(`${identity.binding} is adapter-owned; configure 'signingIdentity' instead of declaring that secret.`);
|
|
5
|
+
}
|
|
6
|
+
return Object.freeze({ ...secrets, [identity.binding]: identity.secret });
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SIGNING_IDENTITY_BINDING } from './binding.js';
|
|
2
|
+
export interface SigningIdentitySource {
|
|
3
|
+
readonly binding: typeof SIGNING_IDENTITY_BINDING;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly secret: string;
|
|
6
|
+
readonly keyId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SigningIdentityInput {
|
|
9
|
+
readonly projectDir: string;
|
|
10
|
+
readonly origin: string;
|
|
11
|
+
readonly source?: string;
|
|
12
|
+
readonly mode: 'local' | 'deploy';
|
|
13
|
+
}
|
|
14
|
+
/** Resolve one admitted Domain signing identity for local or remote provider custody. */
|
|
15
|
+
export declare function signingIdentity(input: SigningIdentityInput): Promise<SigningIdentitySource>;
|
|
16
|
+
/**
|
|
17
|
+
* Read an explicitly retained deployment identity. Deployment never generates
|
|
18
|
+
* one: a missing file is a pre-provider configuration failure, not an implicit
|
|
19
|
+
* key rotation.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readSigningIdentity(projectDir: string, declaration: string | undefined): Promise<SigningIdentitySource>;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve local development identity. The default is generated once inside
|
|
24
|
+
* `.astrale`; an explicit source is always read-only and must already exist.
|
|
25
|
+
*/
|
|
26
|
+
export declare function localSigningIdentity(projectDir: string, declaration: string | undefined, origin: string): Promise<SigningIdentitySource>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { link, mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { SIGNING_IDENTITY_BINDING } from './binding.js';
|
|
6
|
+
import { admitSigningIdentity, createSigningIdentity } from './material.js';
|
|
7
|
+
const DEFAULT_IDENTITY = join('.astrale', 'identity.json');
|
|
8
|
+
/** Resolve one admitted Domain signing identity for local or remote provider custody. */
|
|
9
|
+
export function signingIdentity(input) {
|
|
10
|
+
return input.mode === 'local'
|
|
11
|
+
? localSigningIdentity(input.projectDir, input.source, input.origin)
|
|
12
|
+
: readSigningIdentity(input.projectDir, input.source);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read an explicitly retained deployment identity. Deployment never generates
|
|
16
|
+
* one: a missing file is a pre-provider configuration failure, not an implicit
|
|
17
|
+
* key rotation.
|
|
18
|
+
*/
|
|
19
|
+
export async function readSigningIdentity(projectDir, declaration) {
|
|
20
|
+
if (declaration === undefined || declaration.length === 0) {
|
|
21
|
+
throw new TypeError("Cloudflare deployment requires 'signingIdentity: <private-jwk-file>'. " +
|
|
22
|
+
'Run local development once to create .astrale/identity.json, then retain that file in managed secret storage.');
|
|
23
|
+
}
|
|
24
|
+
return readSource(pathOf(projectDir, declaration), declaration);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve local development identity. The default is generated once inside
|
|
28
|
+
* `.astrale`; an explicit source is always read-only and must already exist.
|
|
29
|
+
*/
|
|
30
|
+
export async function localSigningIdentity(projectDir, declaration, origin) {
|
|
31
|
+
if (declaration !== undefined)
|
|
32
|
+
return readSource(pathOf(projectDir, declaration), declaration);
|
|
33
|
+
const path = join(projectDir, DEFAULT_IDENTITY);
|
|
34
|
+
if (!existsSync(path)) {
|
|
35
|
+
const legacy = join(projectDir, '.astrale', 'identity.ts');
|
|
36
|
+
if (existsSync(legacy))
|
|
37
|
+
await migrateLegacyIdentity(legacy, path, origin);
|
|
38
|
+
else
|
|
39
|
+
await generateIdentity(path);
|
|
40
|
+
}
|
|
41
|
+
return readSource(path, DEFAULT_IDENTITY);
|
|
42
|
+
}
|
|
43
|
+
async function generateIdentity(path) {
|
|
44
|
+
const value = await createSigningIdentity();
|
|
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
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function migrateLegacyIdentity(legacy, destination, origin) {
|
|
51
|
+
const source = await readFile(legacy, 'utf8');
|
|
52
|
+
const match = /export const PRIVATE_JWK = (\{[\s\S]*\}) as const\n?$/u.exec(source);
|
|
53
|
+
if (match?.[1] === undefined)
|
|
54
|
+
throw invalidIdentity(legacy);
|
|
55
|
+
let decoded;
|
|
56
|
+
try {
|
|
57
|
+
decoded = JSON.parse(match[1]);
|
|
58
|
+
}
|
|
59
|
+
catch (cause) {
|
|
60
|
+
throw invalidIdentity(legacy, cause);
|
|
61
|
+
}
|
|
62
|
+
const value = await admitSigningIdentity(decoded, legacy, `${origin}-key`);
|
|
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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function readSource(path, declaration) {
|
|
96
|
+
let source;
|
|
97
|
+
try {
|
|
98
|
+
source = await readFile(path, 'utf8');
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
if (!(cause instanceof Error && 'code' in cause && cause.code === 'ENOENT'))
|
|
102
|
+
throw cause;
|
|
103
|
+
throw new TypeError(`Cloudflare signing identity '${declaration}' is missing at ${path}.`, {
|
|
104
|
+
cause,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
let decoded;
|
|
108
|
+
try {
|
|
109
|
+
decoded = JSON.parse(source);
|
|
110
|
+
}
|
|
111
|
+
catch (cause) {
|
|
112
|
+
throw invalidIdentity(declaration, cause);
|
|
113
|
+
}
|
|
114
|
+
const value = await admitSigningIdentity(decoded, declaration);
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
binding: SIGNING_IDENTITY_BINDING,
|
|
117
|
+
path,
|
|
118
|
+
secret: JSON.stringify(value),
|
|
119
|
+
keyId: value.kid,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function pathOf(projectDir, declaration) {
|
|
123
|
+
return isAbsolute(declaration) ? declaration : resolve(projectDir, declaration);
|
|
124
|
+
}
|
|
125
|
+
function invalidIdentity(owner, cause) {
|
|
126
|
+
return new TypeError(`Cloudflare signing identity '${owner}' is not a private Ed25519 JWK.`, cause === undefined ? undefined : { cause });
|
|
127
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@astrale-os/adapter-cloudflare` — deploy an Astrale
|
|
3
|
-
* Cloudflare
|
|
2
|
+
* `@astrale-os/adapter-cloudflare` — deploy an Astrale application through
|
|
3
|
+
* Cloudflare Workers on YOUR OWN Cloudflare account.
|
|
4
4
|
*
|
|
5
5
|
* import { cloudflare } from '@astrale-os/adapter-cloudflare'
|
|
6
6
|
*
|
|
7
7
|
* adapter: cloudflare({
|
|
8
8
|
* dev: { secrets: '.env.dev' },
|
|
9
|
-
* prod: { route: 'crm.acme.dev', secrets: '.env.prod' },
|
|
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
|
|
@@ -18,4 +19,3 @@
|
|
|
18
19
|
*/
|
|
19
20
|
export { cloudflare } from './cloudflare.js';
|
|
20
21
|
export type { CloudflareParams } from './params.js';
|
|
21
|
-
//# sourceMappingURL=index.d.ts.map
|