@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.
Files changed (120) hide show
  1. package/README.md +11 -0
  2. package/dist/adapter.d.ts +17 -0
  3. package/dist/adapter.js +140 -0
  4. package/dist/build/artifact.d.ts +8 -0
  5. package/dist/build/artifact.js +179 -0
  6. package/dist/build/closure.d.ts +2 -0
  7. package/dist/build/closure.js +22 -0
  8. package/dist/build/codegen/execution.d.ts +13 -0
  9. package/dist/build/codegen/execution.js +44 -0
  10. package/dist/build/codegen/gateway.d.ts +1 -0
  11. package/dist/build/codegen/gateway.js +7 -0
  12. package/dist/build/codegen/merge.d.ts +0 -1
  13. package/dist/build/codegen/merge.js +0 -1
  14. package/dist/build/codegen/wrangler.d.ts +10 -1
  15. package/dist/build/codegen/wrangler.js +22 -16
  16. package/dist/build/configuration.d.ts +9 -0
  17. package/dist/build/configuration.js +40 -0
  18. package/dist/build/frontend-v1.d.ts +16 -0
  19. package/dist/build/frontend-v1.js +133 -0
  20. package/dist/build/index.d.ts +8 -16
  21. package/dist/build/index.js +8 -16
  22. package/dist/build/metafile.d.ts +5 -0
  23. package/dist/build/metafile.js +14 -0
  24. package/dist/build/parse-output.d.ts +0 -1
  25. package/dist/build/parse-output.js +0 -1
  26. package/dist/build/qualification.d.ts +5 -0
  27. package/dist/build/qualification.js +7 -0
  28. package/dist/build/roots.d.ts +2 -0
  29. package/dist/build/roots.js +34 -0
  30. package/dist/build/wrangler-cli.d.ts +53 -34
  31. package/dist/build/wrangler-cli.js +284 -111
  32. package/dist/cloudflare.d.ts +1 -34
  33. package/dist/cloudflare.js +1 -205
  34. package/dist/deploy/deploy.d.ts +17 -0
  35. package/dist/deploy/deploy.js +207 -0
  36. package/dist/deploy/index.d.ts +1 -0
  37. package/dist/deploy/index.js +1 -0
  38. package/dist/identity/binding.d.ts +2 -0
  39. package/dist/identity/binding.js +2 -0
  40. package/dist/identity/material.d.ts +7 -0
  41. package/dist/identity/material.js +42 -0
  42. package/dist/identity/secret.d.ts +3 -0
  43. package/dist/identity/secret.js +7 -0
  44. package/dist/identity/source.d.ts +26 -0
  45. package/dist/identity/source.js +98 -0
  46. package/dist/index.d.ts +3 -4
  47. package/dist/index.js +3 -4
  48. package/dist/params.d.ts +18 -5
  49. package/dist/params.js +0 -1
  50. package/dist/release/index.d.ts +2 -0
  51. package/dist/release/index.js +2 -0
  52. package/dist/release/namespace.d.ts +2 -0
  53. package/dist/release/namespace.js +6 -0
  54. package/dist/release/target.d.ts +2 -0
  55. package/dist/release/target.js +9 -0
  56. package/dist/worker/entry.d.ts +34 -0
  57. package/dist/worker/entry.js +157 -0
  58. package/dist/worker/fetch.d.ts +11 -0
  59. package/dist/worker/fetch.js +31 -0
  60. package/dist/worker/gateway.d.ts +15 -0
  61. package/dist/worker/gateway.js +26 -0
  62. package/dist/worker/identity.d.ts +7 -0
  63. package/dist/worker/identity.js +23 -0
  64. package/dist/worker/index.d.ts +3 -48
  65. package/dist/worker/index.js +3 -186
  66. package/package.json +21 -18
  67. package/template/astrale.config.ts +19 -11
  68. package/dist/build/codegen/identity.d.ts +0 -10
  69. package/dist/build/codegen/identity.d.ts.map +0 -1
  70. package/dist/build/codegen/identity.js +0 -32
  71. package/dist/build/codegen/identity.js.map +0 -1
  72. package/dist/build/codegen/merge.d.ts.map +0 -1
  73. package/dist/build/codegen/merge.js.map +0 -1
  74. package/dist/build/codegen/worker.d.ts +0 -12
  75. package/dist/build/codegen/worker.d.ts.map +0 -1
  76. package/dist/build/codegen/worker.js +0 -40
  77. package/dist/build/codegen/worker.js.map +0 -1
  78. package/dist/build/codegen/wrangler.d.ts.map +0 -1
  79. package/dist/build/codegen/wrangler.js.map +0 -1
  80. package/dist/build/frontend.d.ts +0 -26
  81. package/dist/build/frontend.d.ts.map +0 -1
  82. package/dist/build/frontend.js +0 -263
  83. package/dist/build/frontend.js.map +0 -1
  84. package/dist/build/index.d.ts.map +0 -1
  85. package/dist/build/index.js.map +0 -1
  86. package/dist/build/parse-output.d.ts.map +0 -1
  87. package/dist/build/parse-output.js.map +0 -1
  88. package/dist/build/prepare.d.ts +0 -18
  89. package/dist/build/prepare.d.ts.map +0 -1
  90. package/dist/build/prepare.js +0 -117
  91. package/dist/build/prepare.js.map +0 -1
  92. package/dist/build/worker.d.ts +0 -15
  93. package/dist/build/worker.d.ts.map +0 -1
  94. package/dist/build/worker.js +0 -21
  95. package/dist/build/worker.js.map +0 -1
  96. package/dist/build/wrangler-cli.d.ts.map +0 -1
  97. package/dist/build/wrangler-cli.js.map +0 -1
  98. package/dist/cloudflare.d.ts.map +0 -1
  99. package/dist/cloudflare.js.map +0 -1
  100. package/dist/index.d.ts.map +0 -1
  101. package/dist/index.js.map +0 -1
  102. package/dist/params.d.ts.map +0 -1
  103. package/dist/params.js.map +0 -1
  104. package/dist/worker/index.d.ts.map +0 -1
  105. package/dist/worker/index.js.map +0 -1
  106. package/src/build/codegen/identity.ts +0 -40
  107. package/src/build/codegen/merge.ts +0 -92
  108. package/src/build/codegen/worker.ts +0 -53
  109. package/src/build/codegen/wrangler.ts +0 -127
  110. package/src/build/frontend.ts +0 -324
  111. package/src/build/index.ts +0 -29
  112. package/src/build/parse-output.ts +0 -60
  113. package/src/build/prepare.ts +0 -161
  114. package/src/build/worker.ts +0 -37
  115. package/src/build/wrangler-cli.ts +0 -362
  116. package/src/cloudflare.ts +0 -251
  117. package/src/index.ts +0 -21
  118. package/src/params.ts +0 -92
  119. package/src/worker/index.ts +0 -276
  120. package/src/worker/tsconfig.json +0 -10
@@ -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,2 @@
1
+ /** Adapter-owned Worker secret that carries the execution identity private JWK. */
2
+ export declare const SIGNING_IDENTITY_BINDING = "ASTRALE_SIGNING_IDENTITY";
@@ -0,0 +1,2 @@
1
+ /** Adapter-owned Worker secret that carries the execution identity private JWK. */
2
+ export const SIGNING_IDENTITY_BINDING = 'ASTRALE_SIGNING_IDENTITY';
@@ -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 domain as a standalone
3
- * Cloudflare Worker on YOUR OWN Cloudflare account.
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