@astrale-os/adapter-cloudflare 0.5.0-beta.2 → 0.5.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/adapter.d.ts +17 -0
- package/dist/adapter.js +140 -0
- package/dist/build/artifact.d.ts +8 -0
- package/dist/build/artifact.js +179 -0
- package/dist/build/closure.d.ts +2 -0
- package/dist/build/closure.js +22 -0
- package/dist/build/codegen/execution.d.ts +13 -0
- package/dist/build/codegen/execution.js +44 -0
- package/dist/build/codegen/gateway.d.ts +1 -0
- package/dist/build/codegen/gateway.js +7 -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 +10 -1
- package/dist/build/codegen/wrangler.js +22 -16
- package/dist/build/configuration.d.ts +9 -0
- package/dist/build/configuration.js +40 -0
- package/dist/build/frontend-v1.d.ts +16 -0
- package/dist/build/frontend-v1.js +133 -0
- package/dist/build/index.d.ts +8 -16
- package/dist/build/index.js +8 -16
- package/dist/build/metafile.d.ts +5 -0
- package/dist/build/metafile.js +14 -0
- package/dist/build/parse-output.d.ts +0 -1
- package/dist/build/parse-output.js +0 -1
- 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/wrangler-cli.d.ts +53 -34
- package/dist/build/wrangler-cli.js +284 -111
- package/dist/cloudflare.d.ts +1 -34
- package/dist/cloudflare.js +1 -205
- package/dist/deploy/deploy.d.ts +17 -0
- package/dist/deploy/deploy.js +207 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/index.js +1 -0
- package/dist/identity/binding.d.ts +2 -0
- package/dist/identity/binding.js +2 -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 +98 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -4
- package/dist/params.d.ts +18 -5
- package/dist/params.js +0 -1
- package/dist/release/index.d.ts +2 -0
- package/dist/release/index.js +2 -0
- package/dist/release/namespace.d.ts +2 -0
- package/dist/release/namespace.js +6 -0
- package/dist/release/target.d.ts +2 -0
- package/dist/release/target.js +9 -0
- package/dist/worker/entry.d.ts +34 -0
- package/dist/worker/entry.js +157 -0
- package/dist/worker/fetch.d.ts +11 -0
- package/dist/worker/fetch.js +31 -0
- package/dist/worker/gateway.d.ts +15 -0
- package/dist/worker/gateway.js +26 -0
- package/dist/worker/identity.d.ts +7 -0
- package/dist/worker/identity.js +23 -0
- package/dist/worker/index.d.ts +3 -48
- package/dist/worker/index.js +3 -186
- package/package.json +21 -18
- package/template/astrale.config.ts +19 -11
- 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.map +0 -1
- 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.map +0 -1
- 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/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 -362
- package/src/cloudflare.ts +0 -251
- package/src/index.ts +0 -21
- package/src/params.ts +0 -92
- package/src/worker/index.ts +0 -276
- package/src/worker/tsconfig.json +0 -10
package/dist/cloudflare.js
CHANGED
|
@@ -1,205 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* `cloudflare(envs)` — the Cloudflare deployment adapter.
|
|
3
|
-
*
|
|
4
|
-
* Owns everything target-specific: it codegens the Worker entry + wrangler
|
|
5
|
-
* config (the dev never sees a `wrangler.jsonc`), materializes the optional frontend
|
|
6
|
-
* SPA, and shells out to `wrangler`. `watch` → `wrangler dev` (local URL);
|
|
7
|
-
* `deploy` → `wrangler deploy` (workers.dev or a custom-domain URL); secrets →
|
|
8
|
-
* `wrangler secret bulk`. Both `watch` and `deploy` return the URL the CLI
|
|
9
|
-
* prints and `astrale domain install <url>` consumes.
|
|
10
|
-
*/
|
|
11
|
-
import { defineAdapter } from '@astrale-os/sdk';
|
|
12
|
-
import { bulkPutSecrets, buildCloudflareWorker, buildFrontend, logTo, prepare, runWranglerDeploy, runWranglerDev, runWranglerTypes, watchFrontend, } from './build/index.js';
|
|
13
|
-
export function cloudflare(envs) {
|
|
14
|
-
return cloudflareWithDependencies(envs, {
|
|
15
|
-
bulkPutSecrets,
|
|
16
|
-
buildCloudflareWorker,
|
|
17
|
-
buildFrontend,
|
|
18
|
-
logTo,
|
|
19
|
-
prepare,
|
|
20
|
-
runWranglerDeploy,
|
|
21
|
-
runWranglerDev,
|
|
22
|
-
runWranglerTypes,
|
|
23
|
-
watchFrontend,
|
|
24
|
-
waitUntilLive,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
/** Exact orchestration seam used by adapter-level deployment qualification. */
|
|
28
|
-
export function cloudflareWithDependencies(envs, dependencies) {
|
|
29
|
-
return defineAdapter({
|
|
30
|
-
name: 'cloudflare',
|
|
31
|
-
envs,
|
|
32
|
-
async build(params, ctx) {
|
|
33
|
-
await dependencies.buildCloudflareWorker({
|
|
34
|
-
params,
|
|
35
|
-
context: ctx,
|
|
36
|
-
onLog: dependencies.logTo(),
|
|
37
|
-
});
|
|
38
|
-
},
|
|
39
|
-
async watch(params, ctx) {
|
|
40
|
-
const onLog = dependencies.logTo();
|
|
41
|
-
const frontend = await dependencies.buildFrontend({
|
|
42
|
-
definition: ctx.definition,
|
|
43
|
-
projectDir: ctx.projectDir,
|
|
44
|
-
onLog,
|
|
45
|
-
});
|
|
46
|
-
const { configPath, port } = await dependencies.prepare(params, ctx, 'dev', frontend);
|
|
47
|
-
const handle = await dependencies.runWranglerDev({
|
|
48
|
-
projectDir: ctx.projectDir,
|
|
49
|
-
configPath,
|
|
50
|
-
port,
|
|
51
|
-
remote: Boolean(params.remote),
|
|
52
|
-
autoPickPort: params.port === undefined,
|
|
53
|
-
...(params.host ? { ip: '0.0.0.0' } : {}),
|
|
54
|
-
onReload: ctx.onReload,
|
|
55
|
-
onLog,
|
|
56
|
-
});
|
|
57
|
-
let frontendWatch;
|
|
58
|
-
try {
|
|
59
|
-
frontendWatch = dependencies.watchFrontend({
|
|
60
|
-
definition: ctx.definition,
|
|
61
|
-
projectDir: ctx.projectDir,
|
|
62
|
-
onLog,
|
|
63
|
-
// Routes and source recipes are immutable for this watch generation.
|
|
64
|
-
// Wrangler already observes the atomically swapped asset stage, so a
|
|
65
|
-
// source rebuild needs no config regeneration.
|
|
66
|
-
onRebuild: ctx.onReload,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
try {
|
|
71
|
-
await handle.stop();
|
|
72
|
-
}
|
|
73
|
-
catch (cleanupError) {
|
|
74
|
-
throw new AggregateError([error, cleanupError], 'cloudflare frontend watcher startup failed and Wrangler cleanup also failed');
|
|
75
|
-
}
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
// With --host the actionable URL is the PUBLIC one (the tunnel/proxy
|
|
79
|
-
// front), not the local bind — print and spec-stamp that.
|
|
80
|
-
return {
|
|
81
|
-
url: params.host ?? handle.url,
|
|
82
|
-
async stop() {
|
|
83
|
-
const errors = [];
|
|
84
|
-
try {
|
|
85
|
-
await frontendWatch?.stop();
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
errors.push(error);
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
await handle.stop();
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
errors.push(error);
|
|
95
|
-
}
|
|
96
|
-
if (errors.length === 1)
|
|
97
|
-
throw errors[0];
|
|
98
|
-
if (errors.length > 1) {
|
|
99
|
-
throw new AggregateError(errors, 'cloudflare development cleanup failed');
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
},
|
|
104
|
-
// Config hot-regen: rewrite the generated files only — the running
|
|
105
|
-
// `wrangler dev` watches its `--config` (and the worker entry) and reloads
|
|
106
|
-
// them itself, so a `astrale.config.ts` edit lands without a restart.
|
|
107
|
-
// Mirrors `watch`'s prep exactly (codegen + frontend build), minus the spawn.
|
|
108
|
-
async regenerate(params, ctx) {
|
|
109
|
-
const frontend = await dependencies.buildFrontend({
|
|
110
|
-
definition: ctx.definition,
|
|
111
|
-
projectDir: ctx.projectDir,
|
|
112
|
-
onLog: dependencies.logTo(),
|
|
113
|
-
});
|
|
114
|
-
const { configPath } = await dependencies.prepare(params, ctx, 'dev', frontend);
|
|
115
|
-
await dependencies.runWranglerTypes({
|
|
116
|
-
projectDir: ctx.projectDir,
|
|
117
|
-
configPath,
|
|
118
|
-
onLog: dependencies.logTo(),
|
|
119
|
-
});
|
|
120
|
-
},
|
|
121
|
-
async deploy(params, ctx) {
|
|
122
|
-
const frontend = await dependencies.buildFrontend({
|
|
123
|
-
definition: ctx.definition,
|
|
124
|
-
projectDir: ctx.projectDir,
|
|
125
|
-
onLog: dependencies.logTo(),
|
|
126
|
-
});
|
|
127
|
-
const { configPath, fallbackConfigPath, workerName: name, } = await dependencies.prepare(params, ctx, 'deploy', frontend);
|
|
128
|
-
const { url, deploymentVersion } = await dependencies.runWranglerDeploy({
|
|
129
|
-
projectDir: ctx.projectDir,
|
|
130
|
-
configPath,
|
|
131
|
-
workerName: name,
|
|
132
|
-
...(fallbackConfigPath ? { fallbackConfigPath } : {}),
|
|
133
|
-
...(params.route ? { route: params.route } : {}),
|
|
134
|
-
onLog: dependencies.logTo(),
|
|
135
|
-
});
|
|
136
|
-
if (Object.keys(ctx.secrets).length > 0) {
|
|
137
|
-
await dependencies.bulkPutSecrets({
|
|
138
|
-
projectDir: ctx.projectDir,
|
|
139
|
-
configPath,
|
|
140
|
-
secrets: ctx.secrets,
|
|
141
|
-
onLog: dependencies.logTo(),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
// A first deploy on a fresh `*.workers.dev` host can take ~30-60s to
|
|
145
|
-
// propagate; an `astrale domain install <url>` issued right away would
|
|
146
|
-
// 404 (Cloudflare 1042). Block until the URL actually serves so the
|
|
147
|
-
// install line we print is immediately actionable.
|
|
148
|
-
await dependencies.waitUntilLive(url, dependencies.logTo(), deploymentVersion);
|
|
149
|
-
return { url };
|
|
150
|
-
},
|
|
151
|
-
secretsFile(params) {
|
|
152
|
-
return params.secrets;
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Poll until the deployed Worker health endpoint returns exactly 200.
|
|
158
|
-
*/
|
|
159
|
-
async function waitUntilLive(url, onLog, expectedDeploymentVersion, timeoutMs = 90_000) {
|
|
160
|
-
const probe = new URL('/health', url);
|
|
161
|
-
const metadata = new URL('/meta', url);
|
|
162
|
-
const deadline = Date.now() + timeoutMs;
|
|
163
|
-
let waited = false;
|
|
164
|
-
let lastStatus;
|
|
165
|
-
let lastDeploymentVersion;
|
|
166
|
-
for (;;) {
|
|
167
|
-
try {
|
|
168
|
-
const res = await fetch(probe, { redirect: 'manual' });
|
|
169
|
-
lastStatus = res.status;
|
|
170
|
-
await res.body?.cancel();
|
|
171
|
-
if (res.status === 200) {
|
|
172
|
-
if (expectedDeploymentVersion === undefined)
|
|
173
|
-
return;
|
|
174
|
-
const meta = await fetch(metadata, { redirect: 'manual' });
|
|
175
|
-
if (meta.status === 200) {
|
|
176
|
-
const body = (await meta.json());
|
|
177
|
-
lastDeploymentVersion =
|
|
178
|
-
typeof body.deploymentVersion === 'string' ? body.deploymentVersion : undefined;
|
|
179
|
-
if (lastDeploymentVersion === expectedDeploymentVersion)
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
await meta.body?.cancel();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
// Network hiccup — treat like "not live yet" and keep polling.
|
|
189
|
-
}
|
|
190
|
-
if (Date.now() >= deadline) {
|
|
191
|
-
throw new Error(`deployed Worker health did not reach HTTP 200 within ${Math.round(timeoutMs / 1000)}s` +
|
|
192
|
-
(lastStatus === undefined ? '' : ` (last status ${lastStatus})`) +
|
|
193
|
-
(expectedDeploymentVersion === undefined
|
|
194
|
-
? ''
|
|
195
|
-
: `; deployment version ${JSON.stringify(lastDeploymentVersion)} ` +
|
|
196
|
-
`did not reach ${JSON.stringify(expectedDeploymentVersion)}`));
|
|
197
|
-
}
|
|
198
|
-
if (!waited) {
|
|
199
|
-
waited = true;
|
|
200
|
-
onLog('waiting for the URL to go live (first deploys can take ~30-60s)…');
|
|
201
|
-
}
|
|
202
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
//# sourceMappingURL=cloudflare.js.map
|
|
1
|
+
export { cloudflare } from './adapter.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DeployContext, DeployResult } from '@astrale-os/sdk/deployment/adapter';
|
|
2
|
+
import type { Release } from '@astrale-os/sdk/deployment/release';
|
|
3
|
+
import type { materialize } from '../build/artifact.js';
|
|
4
|
+
import type { ensureWranglerDispatchNamespace, runWranglerDeploy, runWranglerDeploymentStatus, runWranglerReleaseDeploy } from '../build/wrangler-cli.js';
|
|
5
|
+
import type { signingIdentity } from '../identity/source.js';
|
|
6
|
+
import type { CloudflareParams } from '../params.js';
|
|
7
|
+
export interface CloudflareDeployDependencies {
|
|
8
|
+
readonly materialize: typeof materialize;
|
|
9
|
+
readonly ensureWranglerDispatchNamespace: typeof ensureWranglerDispatchNamespace;
|
|
10
|
+
readonly runWranglerDeploy: typeof runWranglerDeploy;
|
|
11
|
+
readonly runWranglerReleaseDeploy: typeof runWranglerReleaseDeploy;
|
|
12
|
+
readonly runWranglerDeploymentStatus: typeof runWranglerDeploymentStatus;
|
|
13
|
+
readonly waitForReadiness: (release: Release) => Promise<void>;
|
|
14
|
+
readonly signingIdentity: typeof signingIdentity;
|
|
15
|
+
}
|
|
16
|
+
/** Establish private placement and place one revision before advancing the public gateway. */
|
|
17
|
+
export declare function deployCloudflare(parameters: CloudflareParams, context: DeployContext, dependencies: CloudflareDeployDependencies): Promise<DeployResult>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { addressing, assemble } from '@astrale-os/sdk/deployment/release';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { workerName } from '../build/configuration.js';
|
|
4
|
+
import { WranglerDeployError, WranglerDispatchNamespaceError } from '../build/wrangler-cli.js';
|
|
5
|
+
import { signingIdentitySecrets } from '../identity/secret.js';
|
|
6
|
+
import { releaseNamespace } from '../release/index.js';
|
|
7
|
+
const NONE = Object.freeze({ kind: 'none' });
|
|
8
|
+
/** Establish private placement and place one revision before advancing the public gateway. */
|
|
9
|
+
export async function deployCloudflare(parameters, context, dependencies) {
|
|
10
|
+
let secrets;
|
|
11
|
+
try {
|
|
12
|
+
const identity = await dependencies.signingIdentity({
|
|
13
|
+
projectDir: context.projectDir,
|
|
14
|
+
source: parameters.signingIdentity,
|
|
15
|
+
origin: context.artifact.build.schema.compiled.root.origin,
|
|
16
|
+
mode: 'deploy',
|
|
17
|
+
});
|
|
18
|
+
secrets = signingIdentitySecrets(context.secrets, identity);
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
return failed('prerequisite', progress({}), cause);
|
|
22
|
+
}
|
|
23
|
+
const directory = join(context.projectDir, '.astrale', 'deploy');
|
|
24
|
+
await dependencies.materialize(directory, context.artifact.files);
|
|
25
|
+
const name = workerName(parameters, context.artifact.build.schema.compiled.root.origin);
|
|
26
|
+
const releaseConfigPath = join(directory, 'wrangler.release.jsonc');
|
|
27
|
+
const gatewayConfigPath = join(directory, 'wrangler.gateway.jsonc');
|
|
28
|
+
const namespace = releaseNamespace(name);
|
|
29
|
+
try {
|
|
30
|
+
await dependencies.ensureWranglerDispatchNamespace({
|
|
31
|
+
projectDir: context.projectDir,
|
|
32
|
+
namespace,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
const provider = cause instanceof WranglerDispatchNamespaceError && cause.effect === 'none'
|
|
37
|
+
? NONE
|
|
38
|
+
: Object.freeze({ kind: 'unknown' });
|
|
39
|
+
return failed('provider', progress({ provider }), cause);
|
|
40
|
+
}
|
|
41
|
+
let revision;
|
|
42
|
+
try {
|
|
43
|
+
revision = await dependencies.runWranglerReleaseDeploy({
|
|
44
|
+
projectDir: context.projectDir,
|
|
45
|
+
configPath: releaseConfigPath,
|
|
46
|
+
namespace,
|
|
47
|
+
secrets,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
const provider = partialProvider(cause, context.artifact);
|
|
52
|
+
const configuration = Object.freeze({ kind: 'unknown' });
|
|
53
|
+
return failed('provider', progress({ provider, configuration }), cause);
|
|
54
|
+
}
|
|
55
|
+
const configuration = Object.freeze({
|
|
56
|
+
kind: 'applied',
|
|
57
|
+
evidence: Object.freeze({ names: Object.keys(secrets).sort() }),
|
|
58
|
+
});
|
|
59
|
+
const retained = Object.freeze({
|
|
60
|
+
kind: 'partial',
|
|
61
|
+
evidence: Object.freeze({
|
|
62
|
+
provider: 'cloudflare',
|
|
63
|
+
deployment: revision.deploymentVersion,
|
|
64
|
+
artifact: context.artifact.digest,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
let gateway;
|
|
68
|
+
try {
|
|
69
|
+
gateway = await dependencies.runWranglerDeploy({
|
|
70
|
+
projectDir: context.projectDir,
|
|
71
|
+
configPath: gatewayConfigPath,
|
|
72
|
+
...(parameters.route === undefined ? {} : { route: parameters.route }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
return failed('provider', progress({ provider: retained, configuration }), cause);
|
|
77
|
+
}
|
|
78
|
+
let selected;
|
|
79
|
+
try {
|
|
80
|
+
selected = observeAddressing(context.artifact, gateway.url, name);
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
return failed('addressing', progress({
|
|
84
|
+
provider: retained,
|
|
85
|
+
configuration,
|
|
86
|
+
}), cause);
|
|
87
|
+
}
|
|
88
|
+
let observed;
|
|
89
|
+
try {
|
|
90
|
+
observed = await dependencies.runWranglerDeploymentStatus({
|
|
91
|
+
projectDir: context.projectDir,
|
|
92
|
+
configPath: gatewayConfigPath,
|
|
93
|
+
workerName: name,
|
|
94
|
+
});
|
|
95
|
+
requireCurrentDeployment(observed, gateway.deploymentVersion);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
return failed('readiness', progress({
|
|
99
|
+
provider: retained,
|
|
100
|
+
configuration,
|
|
101
|
+
}), cause);
|
|
102
|
+
}
|
|
103
|
+
let release;
|
|
104
|
+
try {
|
|
105
|
+
release = assemble(context.artifact.build, selected);
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
return failed('release', progress({ provider: retained, configuration }), cause);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await dependencies.waitForReadiness(release);
|
|
112
|
+
}
|
|
113
|
+
catch (cause) {
|
|
114
|
+
return failed('readiness', progress({ provider: retained, configuration }), cause, release);
|
|
115
|
+
}
|
|
116
|
+
const provider = Object.freeze({
|
|
117
|
+
kind: 'applied',
|
|
118
|
+
evidence: Object.freeze({
|
|
119
|
+
provider: 'cloudflare',
|
|
120
|
+
deployment: revision.deploymentVersion,
|
|
121
|
+
artifact: context.artifact.digest,
|
|
122
|
+
issuer: selected.issuer,
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
status: 'ready',
|
|
127
|
+
phase: 'complete',
|
|
128
|
+
progress: Object.freeze({ ...progress({ configuration }), provider }),
|
|
129
|
+
release,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function partialProvider(cause, artifact) {
|
|
133
|
+
if (!(cause instanceof WranglerDeployError) || cause.applied?.deploymentVersion === undefined) {
|
|
134
|
+
return Object.freeze({ kind: 'unknown' });
|
|
135
|
+
}
|
|
136
|
+
let issuer;
|
|
137
|
+
if (cause.applied.url !== undefined) {
|
|
138
|
+
try {
|
|
139
|
+
issuer = addressing(cause.applied.url).issuer;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// The provider effect remains known when its printed URL is not Addressing.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
kind: 'partial',
|
|
147
|
+
evidence: Object.freeze({
|
|
148
|
+
provider: 'cloudflare',
|
|
149
|
+
deployment: cause.applied.deploymentVersion,
|
|
150
|
+
artifact: artifact.digest,
|
|
151
|
+
...(issuer === undefined ? {} : { issuer }),
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function requireCurrentDeployment(observed, expectedVersion) {
|
|
156
|
+
if (observed.versions.length !== 1 ||
|
|
157
|
+
observed.versions[0].percentage !== 100 ||
|
|
158
|
+
observed.versions[0].version !== expectedVersion) {
|
|
159
|
+
throw new TypeError('Cloudflare gateway is not one exact Worker version at 100%.');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function observeAddressing(artifact, deployedUrl, expectedWorker) {
|
|
163
|
+
if (artifact.addressing.kind === 'configured') {
|
|
164
|
+
if (deployedUrl === undefined)
|
|
165
|
+
return artifact.addressing.addressing;
|
|
166
|
+
const observed = addressing(deployedUrl);
|
|
167
|
+
if (observed.issuer !== artifact.addressing.addressing.issuer) {
|
|
168
|
+
throw new TypeError('Cloudflare deployed URL differs from configured Addressing.');
|
|
169
|
+
}
|
|
170
|
+
return observed;
|
|
171
|
+
}
|
|
172
|
+
if (deployedUrl === undefined) {
|
|
173
|
+
throw new TypeError('Cloudflare did not report provider-assigned Addressing.');
|
|
174
|
+
}
|
|
175
|
+
const observed = addressing(deployedUrl);
|
|
176
|
+
if (artifact.addressing.claim !== expectedWorker) {
|
|
177
|
+
throw new TypeError('PreparedArtifact worker claim differs from the gateway target.');
|
|
178
|
+
}
|
|
179
|
+
const url = new URL(observed.issuer);
|
|
180
|
+
const labels = url.hostname.split('.');
|
|
181
|
+
if (url.protocol !== 'https:' ||
|
|
182
|
+
labels.length < 4 ||
|
|
183
|
+
labels[0] !== artifact.addressing.claim ||
|
|
184
|
+
labels.at(-2) !== 'workers' ||
|
|
185
|
+
labels.at(-1) !== 'dev') {
|
|
186
|
+
throw new TypeError('Cloudflare workers.dev URL does not satisfy its prepared worker claim.');
|
|
187
|
+
}
|
|
188
|
+
return observed;
|
|
189
|
+
}
|
|
190
|
+
function progress(input) {
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
prerequisite: input.prerequisite ?? NONE,
|
|
193
|
+
provider: input.provider ?? NONE,
|
|
194
|
+
configuration: input.configuration ?? NONE,
|
|
195
|
+
installation: input.installation ?? NONE,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function failed(phase, progressValue, cause, release) {
|
|
199
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
status: 'failed',
|
|
202
|
+
phase,
|
|
203
|
+
progress: progressValue,
|
|
204
|
+
error,
|
|
205
|
+
...(release === undefined ? {} : { release }),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deployCloudflare, type CloudflareDeployDependencies } from './deploy.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deployCloudflare } from './deploy.js';
|
|
@@ -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 execution 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,98 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { SIGNING_IDENTITY_BINDING } from './binding.js';
|
|
5
|
+
import { admitSigningIdentity, createSigningIdentity } from './material.js';
|
|
6
|
+
const DEFAULT_IDENTITY = join('.astrale', 'identity.json');
|
|
7
|
+
/** Resolve one admitted execution identity for local or remote provider custody. */
|
|
8
|
+
export function signingIdentity(input) {
|
|
9
|
+
return input.mode === 'local'
|
|
10
|
+
? localSigningIdentity(input.projectDir, input.source, input.origin)
|
|
11
|
+
: readSigningIdentity(input.projectDir, input.source);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read an explicitly retained deployment identity. Deployment never generates
|
|
15
|
+
* one: a missing file is a pre-provider configuration failure, not an implicit
|
|
16
|
+
* key rotation.
|
|
17
|
+
*/
|
|
18
|
+
export async function readSigningIdentity(projectDir, declaration) {
|
|
19
|
+
if (declaration === undefined || declaration.length === 0) {
|
|
20
|
+
throw new TypeError("Cloudflare deployment requires 'signingIdentity: <private-jwk-file>'. " +
|
|
21
|
+
'Run local development once to create .astrale/identity.json, then retain that file in managed secret storage.');
|
|
22
|
+
}
|
|
23
|
+
return readSource(pathOf(projectDir, declaration), declaration);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve local development identity. The default is generated once inside
|
|
27
|
+
* `.astrale`; an explicit source is always read-only and must already exist.
|
|
28
|
+
*/
|
|
29
|
+
export async function localSigningIdentity(projectDir, declaration, origin) {
|
|
30
|
+
if (declaration !== undefined)
|
|
31
|
+
return readSource(pathOf(projectDir, declaration), declaration);
|
|
32
|
+
const path = join(projectDir, DEFAULT_IDENTITY);
|
|
33
|
+
if (!existsSync(path)) {
|
|
34
|
+
const legacy = join(projectDir, '.astrale', 'identity.ts');
|
|
35
|
+
if (existsSync(legacy))
|
|
36
|
+
await migrateLegacyIdentity(legacy, path, origin);
|
|
37
|
+
else
|
|
38
|
+
await generateIdentity(path);
|
|
39
|
+
}
|
|
40
|
+
return readSource(path, DEFAULT_IDENTITY);
|
|
41
|
+
}
|
|
42
|
+
async function generateIdentity(path) {
|
|
43
|
+
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`);
|
|
48
|
+
}
|
|
49
|
+
async function migrateLegacyIdentity(legacy, destination, origin) {
|
|
50
|
+
const source = await readFile(legacy, 'utf8');
|
|
51
|
+
const match = /export const PRIVATE_JWK = (\{[\s\S]*\}) as const\n?$/u.exec(source);
|
|
52
|
+
if (match?.[1] === undefined)
|
|
53
|
+
throw invalidIdentity(legacy);
|
|
54
|
+
let decoded;
|
|
55
|
+
try {
|
|
56
|
+
decoded = JSON.parse(match[1]);
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
throw invalidIdentity(legacy, cause);
|
|
60
|
+
}
|
|
61
|
+
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`);
|
|
65
|
+
}
|
|
66
|
+
async function readSource(path, declaration) {
|
|
67
|
+
let source;
|
|
68
|
+
try {
|
|
69
|
+
source = await readFile(path, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
if (!(cause instanceof Error && 'code' in cause && cause.code === 'ENOENT'))
|
|
73
|
+
throw cause;
|
|
74
|
+
throw new TypeError(`Cloudflare signing identity '${declaration}' is missing at ${path}.`, {
|
|
75
|
+
cause,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
let decoded;
|
|
79
|
+
try {
|
|
80
|
+
decoded = JSON.parse(source);
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
throw invalidIdentity(declaration, cause);
|
|
84
|
+
}
|
|
85
|
+
const value = await admitSigningIdentity(decoded, declaration);
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
binding: SIGNING_IDENTITY_BINDING,
|
|
88
|
+
path,
|
|
89
|
+
secret: JSON.stringify(value),
|
|
90
|
+
keyId: value.kid,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function pathOf(projectDir, declaration) {
|
|
94
|
+
return isAbsolute(declaration) ? declaration : resolve(projectDir, declaration);
|
|
95
|
+
}
|
|
96
|
+
function invalidIdentity(owner, cause) {
|
|
97
|
+
return new TypeError(`Cloudflare signing identity '${owner}' is not a private Ed25519 JWK.`, cause === undefined ? undefined : { cause });
|
|
98
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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
12
|
* `dev` runs `wrangler dev` locally; an env with no `route` ships to
|
|
@@ -18,4 +18,3 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export { cloudflare } from './cloudflare.js';
|
|
20
20
|
export type { CloudflareParams } from './params.js';
|
|
21
|
-
//# sourceMappingURL=index.d.ts.map
|