@astrale-os/adapter-cloudflare 0.5.0-beta.8 → 0.5.0-beta.80

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 (192) hide show
  1. package/CHANGELOG.md +983 -0
  2. package/README.md +20 -2
  3. package/dist/adapter.d.ts +15 -0
  4. package/dist/adapter.js +76 -0
  5. package/dist/build/artifact.d.ts +7 -0
  6. package/dist/build/artifact.js +165 -0
  7. package/dist/build/bundle/admit.d.ts +1 -0
  8. package/dist/build/bundle/admit.js +9 -0
  9. package/dist/build/closure.d.ts +2 -0
  10. package/dist/build/closure.js +22 -0
  11. package/dist/build/codegen/execution.d.ts +3 -0
  12. package/dist/build/codegen/execution.js +2 -0
  13. package/dist/build/codegen/merge.d.ts +0 -1
  14. package/dist/build/codegen/merge.js +0 -1
  15. package/dist/build/codegen/wrangler.d.ts +13 -1
  16. package/dist/build/codegen/wrangler.js +35 -18
  17. package/dist/build/configuration/addressing.d.ts +2 -0
  18. package/dist/build/configuration/addressing.js +11 -0
  19. package/dist/build/configuration/invocation-timeout.d.ts +2 -0
  20. package/dist/build/configuration/invocation-timeout.js +9 -0
  21. package/dist/build/configuration/request-limit.d.ts +2 -0
  22. package/dist/build/configuration/request-limit.js +9 -0
  23. package/dist/build/configuration/router.d.ts +7 -0
  24. package/dist/build/configuration/router.js +11 -0
  25. package/dist/build/configuration/worker-name.d.ts +2 -0
  26. package/dist/build/configuration/worker-name.js +18 -0
  27. package/dist/build/configuration.d.ts +5 -0
  28. package/dist/build/configuration.js +5 -0
  29. package/dist/build/direct.d.ts +4 -0
  30. package/dist/build/direct.js +93 -0
  31. package/dist/build/frontend/files.d.ts +7 -0
  32. package/dist/build/frontend/files.js +60 -0
  33. package/dist/build/frontend/routes.d.ts +2 -0
  34. package/dist/build/frontend/routes.js +11 -0
  35. package/dist/build/index.d.ts +10 -16
  36. package/dist/build/index.js +9 -16
  37. package/dist/build/materialize.d.ts +4 -0
  38. package/dist/build/materialize.js +10 -0
  39. package/dist/build/metafile.d.ts +5 -0
  40. package/dist/build/metafile.js +14 -0
  41. package/dist/build/qualification.d.ts +5 -0
  42. package/dist/build/qualification.js +7 -0
  43. package/dist/build/roots.d.ts +2 -0
  44. package/dist/build/roots.js +34 -0
  45. package/dist/build/worker/artifact.d.ts +40 -0
  46. package/dist/build/worker/artifact.js +1 -0
  47. package/dist/build/worker/artifact.load.d.ts +3 -0
  48. package/dist/build/worker/artifact.load.js +308 -0
  49. package/dist/build/worker/artifact.manifest.d.ts +48 -0
  50. package/dist/build/worker/artifact.manifest.js +87 -0
  51. package/dist/build/worker/artifact.paths.d.ts +4 -0
  52. package/dist/build/worker/artifact.paths.js +25 -0
  53. package/dist/build/worker/codegen/entry.d.ts +2 -0
  54. package/dist/build/worker/codegen/entry.js +28 -0
  55. package/dist/build/worker/codegen/service.d.ts +16 -0
  56. package/dist/build/worker/codegen/service.js +52 -0
  57. package/dist/build/worker/execution.prepare.d.ts +18 -0
  58. package/dist/build/worker/execution.prepare.js +109 -0
  59. package/dist/build/worker/index.d.ts +2 -0
  60. package/dist/build/worker/index.js +1 -0
  61. package/dist/build/worker/modules.d.ts +11 -0
  62. package/dist/build/worker/modules.js +26 -0
  63. package/dist/build/worker/runtime.d.ts +10 -0
  64. package/dist/build/worker/runtime.js +35 -0
  65. package/dist/build/worker/specifier.d.ts +1 -0
  66. package/dist/build/worker/specifier.js +5 -0
  67. package/dist/cloudflare.d.ts +1 -34
  68. package/dist/cloudflare.js +1 -205
  69. package/dist/deploy/deploy.d.ts +16 -0
  70. package/dist/deploy/deploy.js +204 -0
  71. package/dist/deploy/index.d.ts +1 -0
  72. package/dist/deploy/index.js +1 -0
  73. package/dist/deploy/output.d.ts +2 -0
  74. package/dist/deploy/output.js +10 -0
  75. package/dist/development/develop.d.ts +15 -0
  76. package/dist/development/develop.js +267 -0
  77. package/dist/development/frontend.d.ts +11 -0
  78. package/dist/development/frontend.js +109 -0
  79. package/dist/development/host.d.ts +2 -0
  80. package/dist/development/host.js +46 -0
  81. package/dist/development/port.d.ts +1 -0
  82. package/dist/development/port.js +12 -0
  83. package/dist/development/readiness.d.ts +14 -0
  84. package/dist/development/readiness.js +71 -0
  85. package/dist/development/resolved-fetch.d.ts +17 -0
  86. package/dist/development/resolved-fetch.js +80 -0
  87. package/dist/development/worker-output.d.ts +2 -0
  88. package/dist/development/worker-output.js +25 -0
  89. package/dist/development/worker.d.ts +17 -0
  90. package/dist/development/worker.js +147 -0
  91. package/dist/identity/binding.d.ts +2 -0
  92. package/dist/identity/binding.js +2 -0
  93. package/dist/identity/material.d.ts +7 -0
  94. package/dist/identity/material.js +42 -0
  95. package/dist/identity/secret.d.ts +3 -0
  96. package/dist/identity/secret.js +7 -0
  97. package/dist/identity/source.d.ts +26 -0
  98. package/dist/identity/source.js +127 -0
  99. package/dist/index.d.ts +5 -5
  100. package/dist/index.js +5 -5
  101. package/dist/params.d.ts +32 -7
  102. package/dist/params.js +0 -1
  103. package/dist/release/deployment.d.ts +44 -0
  104. package/dist/release/deployment.js +141 -0
  105. package/dist/release/index.d.ts +2 -0
  106. package/dist/release/index.js +2 -0
  107. package/dist/release/staged-worker-name.d.ts +3 -0
  108. package/dist/release/staged-worker-name.js +10 -0
  109. package/dist/worker/entry.d.ts +43 -0
  110. package/dist/worker/entry.js +181 -0
  111. package/dist/worker/fetch.d.ts +0 -1
  112. package/dist/worker/fetch.js +0 -1
  113. package/dist/worker/identity.d.ts +7 -0
  114. package/dist/worker/identity.js +23 -0
  115. package/dist/worker/index.d.ts +2 -61
  116. package/dist/worker/index.js +2 -212
  117. package/dist/wrangler/bundle.d.ts +10 -0
  118. package/dist/wrangler/bundle.js +27 -0
  119. package/dist/wrangler/deploy.d.ts +24 -0
  120. package/dist/wrangler/deploy.js +44 -0
  121. package/dist/wrangler/index.d.ts +5 -0
  122. package/dist/wrangler/index.js +5 -0
  123. package/dist/wrangler/process.d.ts +17 -0
  124. package/dist/wrangler/process.js +115 -0
  125. package/dist/wrangler/secrets.d.ts +6 -0
  126. package/dist/wrangler/secrets.js +58 -0
  127. package/dist/wrangler/status.d.ts +14 -0
  128. package/dist/wrangler/status.js +56 -0
  129. package/dist/wrangler/type-generation.d.ts +6 -0
  130. package/dist/wrangler/type-generation.js +19 -0
  131. package/package.json +30 -19
  132. package/template/astrale.config.ts +25 -17
  133. package/dist/build/codegen/identity.d.ts +0 -10
  134. package/dist/build/codegen/identity.d.ts.map +0 -1
  135. package/dist/build/codegen/identity.js +0 -32
  136. package/dist/build/codegen/identity.js.map +0 -1
  137. package/dist/build/codegen/merge.d.ts.map +0 -1
  138. package/dist/build/codegen/merge.js.map +0 -1
  139. package/dist/build/codegen/worker.d.ts +0 -12
  140. package/dist/build/codegen/worker.d.ts.map +0 -1
  141. package/dist/build/codegen/worker.js +0 -40
  142. package/dist/build/codegen/worker.js.map +0 -1
  143. package/dist/build/codegen/wrangler.d.ts.map +0 -1
  144. package/dist/build/codegen/wrangler.js.map +0 -1
  145. package/dist/build/frontend.d.ts +0 -26
  146. package/dist/build/frontend.d.ts.map +0 -1
  147. package/dist/build/frontend.js +0 -263
  148. package/dist/build/frontend.js.map +0 -1
  149. package/dist/build/index.d.ts.map +0 -1
  150. package/dist/build/index.js.map +0 -1
  151. package/dist/build/parse-output.d.ts +0 -27
  152. package/dist/build/parse-output.d.ts.map +0 -1
  153. package/dist/build/parse-output.js +0 -58
  154. package/dist/build/parse-output.js.map +0 -1
  155. package/dist/build/prepare.d.ts +0 -18
  156. package/dist/build/prepare.d.ts.map +0 -1
  157. package/dist/build/prepare.js +0 -117
  158. package/dist/build/prepare.js.map +0 -1
  159. package/dist/build/worker.d.ts +0 -15
  160. package/dist/build/worker.d.ts.map +0 -1
  161. package/dist/build/worker.js +0 -21
  162. package/dist/build/worker.js.map +0 -1
  163. package/dist/build/wrangler-cli.d.ts +0 -88
  164. package/dist/build/wrangler-cli.d.ts.map +0 -1
  165. package/dist/build/wrangler-cli.js +0 -283
  166. package/dist/build/wrangler-cli.js.map +0 -1
  167. package/dist/cloudflare.d.ts.map +0 -1
  168. package/dist/cloudflare.js.map +0 -1
  169. package/dist/index.d.ts.map +0 -1
  170. package/dist/index.js.map +0 -1
  171. package/dist/params.d.ts.map +0 -1
  172. package/dist/params.js.map +0 -1
  173. package/dist/worker/fetch.d.ts.map +0 -1
  174. package/dist/worker/fetch.js.map +0 -1
  175. package/dist/worker/index.d.ts.map +0 -1
  176. package/dist/worker/index.js.map +0 -1
  177. package/src/build/codegen/identity.ts +0 -40
  178. package/src/build/codegen/merge.ts +0 -92
  179. package/src/build/codegen/worker.ts +0 -53
  180. package/src/build/codegen/wrangler.ts +0 -127
  181. package/src/build/frontend.ts +0 -324
  182. package/src/build/index.ts +0 -29
  183. package/src/build/parse-output.ts +0 -60
  184. package/src/build/prepare.ts +0 -161
  185. package/src/build/worker.ts +0 -37
  186. package/src/build/wrangler-cli.ts +0 -372
  187. package/src/cloudflare.ts +0 -251
  188. package/src/index.ts +0 -21
  189. package/src/params.ts +0 -92
  190. package/src/worker/fetch.ts +0 -46
  191. package/src/worker/index.ts +0 -325
  192. package/src/worker/tsconfig.json +0 -10
@@ -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
+ export declare function parseDeployUrl(text: string, route?: string): string | undefined;
2
+ export declare function parseDeployVersionId(text: string): string | undefined;
@@ -0,0 +1,10 @@
1
+ const WORKERS_DEV_RE = /(https:\/\/[^\s]+\.workers\.dev)/iu;
2
+ const VERSION_ID_RE = /(?:Current\s+)?Version ID:\s*([a-z0-9_-]+)/iu;
3
+ export function parseDeployUrl(text, route) {
4
+ if (route !== undefined)
5
+ return `https://${route.replace(/\/.*$/u, '')}`;
6
+ return WORKERS_DEV_RE.exec(text)?.[1];
7
+ }
8
+ export function parseDeployVersionId(text) {
9
+ return VERSION_ID_RE.exec(text)?.[1];
10
+ }
@@ -0,0 +1,15 @@
1
+ import type { DevelopmentContext, DevelopmentSession } from '@astrale-os/sdk/deployment/adapter';
2
+ import type { CloudflareParams } from '../params.js';
3
+ import { signingIdentity } from '../identity/source.js';
4
+ import { startFrontend } from './frontend.js';
5
+ import { allocatePort } from './port.js';
6
+ import { verifyDevelopmentRelease } from './readiness.js';
7
+ import { startWorker } from './worker.js';
8
+ export interface CloudflareDevelopmentDependencies {
9
+ readonly signingIdentity: typeof signingIdentity;
10
+ readonly startFrontend: typeof startFrontend;
11
+ readonly startWorker: typeof startWorker;
12
+ readonly allocatePort: typeof allocatePort;
13
+ readonly verify: typeof verifyDevelopmentRelease;
14
+ }
15
+ export declare function developCloudflare(parameters: CloudflareParams, context: DevelopmentContext, dependencies?: CloudflareDevelopmentDependencies): Promise<DevelopmentSession>;
@@ -0,0 +1,267 @@
1
+ import { addressing, assemble } from '@astrale-os/sdk/deployment/release';
2
+ import { mkdir, rename, rm, unlink, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { generateWranglerConfig } from '../build/codegen/wrangler.js';
5
+ import { resolveRouter, workerName } from '../build/configuration.js';
6
+ import { loadWorkerArtifact } from '../build/worker/index.js';
7
+ import { workerModules } from '../build/worker/modules.js';
8
+ import { signingIdentitySecrets } from '../identity/secret.js';
9
+ import { signingIdentity } from '../identity/source.js';
10
+ import { startFrontend } from './frontend.js';
11
+ import { admitHost } from './host.js';
12
+ import { allocatePort } from './port.js';
13
+ import { verifyDevelopmentRelease } from './readiness.js';
14
+ import { startWorker } from './worker.js';
15
+ export async function developCloudflare(parameters, context, dependencies = {
16
+ signingIdentity,
17
+ startFrontend,
18
+ startWorker,
19
+ allocatePort,
20
+ verify: verifyDevelopmentRelease,
21
+ }) {
22
+ const hostSource = context.overrides.publicUrl ?? parameters.host;
23
+ const host = hostSource === undefined ? undefined : admitHost(hostSource);
24
+ const configuredPort = context.overrides.localPort ?? parameters.port;
25
+ if (host !== undefined && configuredPort === undefined) {
26
+ throw new TypeError('Cloudflare development with a public host requires a strict local port.');
27
+ }
28
+ const port = configuredPort ?? (await dependencies.allocatePort());
29
+ const directory = join(context.projectDir, '.astrale', 'development', context.environment, 'cloudflare');
30
+ await mkdir(directory, { recursive: true });
31
+ const configPath = join(directory, 'wrangler.development.jsonc');
32
+ const identity = await dependencies.signingIdentity({
33
+ projectDir: context.projectDir,
34
+ source: parameters.signingIdentity,
35
+ origin: context.artifact.build.schema.compiled.root.origin,
36
+ mode: 'local',
37
+ });
38
+ let current = Object.freeze({ artifact: context.artifact, secrets: context.secrets });
39
+ let artifactPaths = new Set();
40
+ let resources;
41
+ let stopping = false;
42
+ let resolveFinished;
43
+ let rejectFinished;
44
+ const finished = new Promise((resolve, reject) => {
45
+ resolveFinished = resolve;
46
+ rejectFinished = reject;
47
+ });
48
+ const realize = async (input) => {
49
+ artifactPaths = await applyArtifact(directory, artifactPaths, input.artifact);
50
+ if (input.artifact.build.frontend?.source.kind === 'vite') {
51
+ await mkdir(join(directory, 'frontend'), { recursive: true });
52
+ }
53
+ const frontend = await dependencies.startFrontend({
54
+ frontend: input.artifact.build.frontend,
55
+ projectDir: context.projectDir,
56
+ });
57
+ try {
58
+ await writeFile(configPath, configuration(parameters, input.artifact, frontend?.url, host, Object.keys(signingIdentitySecrets(input.secrets, identity)).sort()));
59
+ const worker = await dependencies.startWorker({
60
+ projectDir: context.projectDir,
61
+ configPath,
62
+ port,
63
+ remote: parameters.remote ?? false,
64
+ secrets: signingIdentitySecrets(input.secrets, identity),
65
+ autoPickPort: false,
66
+ pinLocalOrigin: host === undefined && parameters.remote !== true,
67
+ ...(host === undefined ? {} : { ip: '0.0.0.0' }),
68
+ });
69
+ const value = Object.freeze({ ...(frontend === undefined ? {} : { frontend }), worker });
70
+ supervise(value);
71
+ return value;
72
+ }
73
+ catch (cause) {
74
+ await frontend?.stop().catch(() => undefined);
75
+ throw cause;
76
+ }
77
+ };
78
+ const supervise = (selected) => {
79
+ for (const child of [selected.worker.finished, selected.frontend?.finished]) {
80
+ child?.catch((cause) => {
81
+ if (!stopping && resources === selected)
82
+ rejectFinished(cause);
83
+ });
84
+ }
85
+ };
86
+ const placementFor = (selected) => Object.freeze({ releaseUrl: host ?? selected.worker.url, localUrl: selected.worker.url });
87
+ const verify = async (input, selected) => {
88
+ const placement = placementFor(selected);
89
+ const release = assemble(input.artifact.build, addressing(placement.releaseUrl));
90
+ await dependencies.verify({ release, localUrl: placement.localUrl, signal: context.signal });
91
+ await context.onPlacement(placement);
92
+ };
93
+ try {
94
+ resources = await realize(current);
95
+ await verify(current, resources);
96
+ }
97
+ catch (cause) {
98
+ stopping = true;
99
+ if (resources !== undefined)
100
+ await stopResources(resources).catch(() => undefined);
101
+ await rm(directory, { recursive: true, force: true }).catch(() => undefined);
102
+ throw cause;
103
+ }
104
+ let updateTail = Promise.resolve();
105
+ const session = Object.freeze({
106
+ current: () => placementFor(resources),
107
+ update(next) {
108
+ if (stopping)
109
+ return Promise.reject(new Error('Cloudflare development session is stopping.'));
110
+ const operation = updateTail.then(async () => {
111
+ if (stopping)
112
+ throw new Error('Cloudflare development session is stopping.');
113
+ sameDomain(current.artifact, next.artifact);
114
+ const previous = current;
115
+ const previousResources = resources;
116
+ const restart = frontendSignature(previous.artifact) !== frontendSignature(next.artifact) ||
117
+ stable(previous.secrets) !== stable(next.secrets);
118
+ if (!restart) {
119
+ try {
120
+ artifactPaths = await applyArtifact(directory, artifactPaths, next.artifact);
121
+ await writeFile(configPath, configuration(parameters, next.artifact, resources.frontend?.url, host, Object.keys(signingIdentitySecrets(next.secrets, identity)).sort()));
122
+ await verify(next, resources);
123
+ current = Object.freeze({ artifact: next.artifact, secrets: next.secrets });
124
+ }
125
+ catch (cause) {
126
+ artifactPaths = await applyArtifact(directory, artifactPaths, previous.artifact);
127
+ await writeFile(configPath, configuration(parameters, previous.artifact, resources.frontend?.url, host, Object.keys(signingIdentitySecrets(previous.secrets, identity)).sort()));
128
+ throw cause;
129
+ }
130
+ return;
131
+ }
132
+ try {
133
+ await stopResources(previousResources);
134
+ }
135
+ catch (cause) {
136
+ rejectFinished(cause);
137
+ throw cause;
138
+ }
139
+ let replacement;
140
+ try {
141
+ replacement = await realize(next);
142
+ await verify(next, replacement);
143
+ resources = replacement;
144
+ current = Object.freeze({ artifact: next.artifact, secrets: next.secrets });
145
+ }
146
+ catch (cause) {
147
+ if (replacement !== undefined)
148
+ await stopResources(replacement).catch(() => undefined);
149
+ try {
150
+ resources = await realize(previous);
151
+ await verify(previous, resources);
152
+ }
153
+ catch (rollback) {
154
+ rejectFinished(new AggregateError([cause, rollback], 'Cloudflare development update and rollback failed.'));
155
+ }
156
+ throw cause;
157
+ }
158
+ });
159
+ updateTail = operation.catch(() => undefined);
160
+ return operation;
161
+ },
162
+ finished,
163
+ async stop() {
164
+ if (stopping)
165
+ return finished.catch(() => undefined);
166
+ stopping = true;
167
+ await updateTail;
168
+ try {
169
+ try {
170
+ await stopResources(resources);
171
+ }
172
+ finally {
173
+ await rm(directory, { recursive: true, force: true });
174
+ }
175
+ resolveFinished();
176
+ }
177
+ catch (cause) {
178
+ rejectFinished(cause);
179
+ throw cause;
180
+ }
181
+ },
182
+ });
183
+ return session;
184
+ }
185
+ function configuration(parameters, artifact, frontendUrl, host, secretBindings) {
186
+ const worker = loadWorkerArtifact(artifact);
187
+ const router = parameters.remote === true || parameters.router !== undefined
188
+ ? resolveRouter(parameters.router)
189
+ : undefined;
190
+ return generateWranglerConfig({
191
+ workerName: workerName(parameters, artifact.build.schema.compiled.root.origin),
192
+ main: `./${worker.release.entrypoint}`,
193
+ modules: workerModules(worker.release),
194
+ vars: {
195
+ ...parameters.vars,
196
+ ...(host === undefined ? {} : { WORKER_URL: host }),
197
+ ...(frontendUrl === undefined ? {} : { FRONTEND_DEV_URL: frontendUrl }),
198
+ },
199
+ secretBindings,
200
+ ...(artifact.build.frontend?.source.kind === 'vite'
201
+ ? { frontend: { directory: './frontend', routes: frontendRoutes(artifact), dev: true } }
202
+ : {}),
203
+ ...(router === undefined
204
+ ? {}
205
+ : { router: { binding: router.binding, service: router.service } }),
206
+ ...(parameters.wrangler === undefined ? {} : { wrangler: parameters.wrangler }),
207
+ selfBinding: true,
208
+ });
209
+ }
210
+ async function applyArtifact(root, previous, artifact) {
211
+ const next = new Set(artifact.files.map((file) => file.path));
212
+ for (const file of artifact.files) {
213
+ const path = join(root, file.path);
214
+ const staging = `${path}.next`;
215
+ await mkdir(dirname(path), { recursive: true });
216
+ await writeFile(staging, file.bytes());
217
+ await rename(staging, path);
218
+ }
219
+ for (const path of previous) {
220
+ if (!next.has(path))
221
+ await unlink(join(root, path)).catch(() => undefined);
222
+ }
223
+ return next;
224
+ }
225
+ async function stopResources(resources) {
226
+ const results = await Promise.allSettled([
227
+ resources.worker.stop(),
228
+ resources.frontend?.stop() ?? Promise.resolve(),
229
+ ]);
230
+ const failures = results.filter((result) => result.status === 'rejected');
231
+ if (failures.length === 1)
232
+ throw failures[0].reason;
233
+ if (failures.length > 1) {
234
+ throw new AggregateError(failures.map((failure) => failure.reason), 'Cloudflare development cleanup failed.');
235
+ }
236
+ }
237
+ function frontendRoutes(artifact) {
238
+ const frontend = artifact.build.frontend;
239
+ if (frontend === undefined || frontend.source.kind !== 'vite')
240
+ return [];
241
+ return Object.freeze(frontend.routes
242
+ .flatMap((route) => frontend.source.kind === 'vite' && frontend.source.spa && route.path !== '/'
243
+ ? [route.path, `${route.path}/*`]
244
+ : route.path === '/'
245
+ ? ['/*']
246
+ : [route.path])
247
+ .sort());
248
+ }
249
+ function frontendSignature(artifact) {
250
+ return stable(artifact.build.frontend);
251
+ }
252
+ function sameDomain(previous, next) {
253
+ if (previous.build.schema.compiled.root.origin !== next.build.schema.compiled.root.origin) {
254
+ throw new TypeError('Cloudflare development update changed the Domain origin.');
255
+ }
256
+ }
257
+ function stable(input) {
258
+ if (input === null || typeof input !== 'object')
259
+ return JSON.stringify(input);
260
+ if (Array.isArray(input))
261
+ return `[${input.map(stable).join(',')}]`;
262
+ const value = input;
263
+ return `{${Object.keys(value)
264
+ .sort()
265
+ .map((key) => `${JSON.stringify(key)}:${stable(value[key])}`)
266
+ .join(',')}}`;
267
+ }
@@ -0,0 +1,11 @@
1
+ import type { Build } from '@astrale-os/sdk/deployment/build';
2
+ export interface FrontendHandle {
3
+ readonly url: string;
4
+ readonly finished: Promise<void>;
5
+ stop(): Promise<void>;
6
+ }
7
+ export declare function startFrontend(input: {
8
+ readonly frontend: Build['frontend'];
9
+ readonly projectDir: string;
10
+ readonly onLog?: (line: string) => void;
11
+ }): Promise<FrontendHandle | undefined>;
@@ -0,0 +1,109 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { createServer } from 'node:net';
4
+ import { join } from 'node:path';
5
+ export async function startFrontend(input) {
6
+ const frontend = input.frontend;
7
+ if (frontend === undefined || frontend.source.kind !== 'vite')
8
+ return undefined;
9
+ const port = await ephemeralPort();
10
+ const child = spawn(viteCommand(input.projectDir), [
11
+ '--host',
12
+ '127.0.0.1',
13
+ '--port',
14
+ String(port),
15
+ '--strictPort',
16
+ ...(frontend.source.config === undefined ? [] : ['--config', frontend.source.config]),
17
+ ], { cwd: input.projectDir, stdio: ['ignore', 'pipe', 'pipe'] });
18
+ const output = [];
19
+ const collect = (buffer) => {
20
+ for (const line of buffer.toString().split('\n')) {
21
+ if (line.trim() === '')
22
+ continue;
23
+ output.push(line);
24
+ if (output.length > 40)
25
+ output.shift();
26
+ input.onLog?.(line);
27
+ }
28
+ };
29
+ child.stdout?.on('data', collect);
30
+ child.stderr?.on('data', collect);
31
+ const url = `http://127.0.0.1:${port}`;
32
+ try {
33
+ await ready(url, child, output);
34
+ }
35
+ catch (cause) {
36
+ await stopChild(child);
37
+ throw cause;
38
+ }
39
+ let stopping = false;
40
+ const finished = child.exitCode === null
41
+ ? new Promise((resolve, reject) => {
42
+ child.once('exit', (code, signal) => {
43
+ if (stopping)
44
+ resolve();
45
+ else {
46
+ reject(new Error(`Vite stopped unexpectedly (code ${code ?? 'null'}, signal ${signal ?? 'none'}).\n${output.join('\n')}`));
47
+ }
48
+ });
49
+ child.once('error', reject);
50
+ })
51
+ : Promise.reject(new Error(`Vite stopped unexpectedly (code ${child.exitCode}).`));
52
+ return Object.freeze({
53
+ url,
54
+ finished,
55
+ async stop() {
56
+ if (stopping)
57
+ return finished.catch(() => undefined);
58
+ stopping = true;
59
+ await stopChild(child);
60
+ },
61
+ });
62
+ }
63
+ function viteCommand(projectDir) {
64
+ const command = join(projectDir, 'node_modules', '.bin', 'vite');
65
+ if (!existsSync(command))
66
+ throw new TypeError('Vite frontend requires a project-local vite binary.');
67
+ return command;
68
+ }
69
+ function ephemeralPort() {
70
+ return new Promise((resolve, reject) => {
71
+ const server = createServer();
72
+ server.once('error', reject);
73
+ server.listen(0, '127.0.0.1', () => {
74
+ const address = server.address();
75
+ const port = typeof address === 'object' && address !== null ? address.port : 0;
76
+ server.close((cause) => (cause === undefined ? resolve(port) : reject(cause)));
77
+ });
78
+ });
79
+ }
80
+ async function ready(url, child, output) {
81
+ const deadline = Date.now() + 15_000;
82
+ while (Date.now() < deadline) {
83
+ if (child.exitCode !== null) {
84
+ throw new Error(`Vite exited before readiness (code ${child.exitCode}).\n${output.join('\n')}`);
85
+ }
86
+ try {
87
+ const response = await fetch(url, { signal: AbortSignal.timeout(500) });
88
+ await response.body?.cancel();
89
+ return;
90
+ }
91
+ catch {
92
+ await new Promise((resolve) => setTimeout(resolve, 50));
93
+ }
94
+ }
95
+ throw new Error(`Vite did not become ready at ${url}.`);
96
+ }
97
+ function stopChild(child) {
98
+ if (child.exitCode !== null)
99
+ return Promise.resolve();
100
+ return new Promise((resolve) => {
101
+ child.once('exit', () => resolve());
102
+ child.kill('SIGTERM');
103
+ setTimeout(() => {
104
+ if (child.exitCode === null)
105
+ child.kill('SIGKILL');
106
+ resolve();
107
+ }, 4_000).unref?.();
108
+ });
109
+ }
@@ -0,0 +1,2 @@
1
+ /** Admit one externally owned public origin without inferring lifecycle ownership. */
2
+ export declare function admitHost(input: string): string;
@@ -0,0 +1,46 @@
1
+ /** Admit one externally owned public origin without inferring lifecycle ownership. */
2
+ export function admitHost(input) {
3
+ let url;
4
+ try {
5
+ url = new URL(input);
6
+ }
7
+ catch (cause) {
8
+ throw new TypeError('Cloudflare development host must be one public HTTP(S) origin.', { cause });
9
+ }
10
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
11
+ url.username !== '' ||
12
+ url.password !== '' ||
13
+ url.pathname !== '/' ||
14
+ url.search !== '' ||
15
+ url.hash !== '' ||
16
+ privateHostname(url.hostname)) {
17
+ throw new TypeError('Cloudflare development host must be one public HTTP(S) origin.');
18
+ }
19
+ return url.origin;
20
+ }
21
+ function privateHostname(input) {
22
+ const host = input
23
+ .toLowerCase()
24
+ .replace(/^\[|\]$/gu, '')
25
+ .replace(/\.$/u, '');
26
+ if (host === 'localhost' || host.endsWith('.localhost'))
27
+ return true;
28
+ if (isIP(host) === 4) {
29
+ const parts = host.split('.').map(Number);
30
+ return (parts[0] === 0 ||
31
+ parts[0] === 10 ||
32
+ parts[0] === 127 ||
33
+ (parts[0] === 169 && parts[1] === 254) ||
34
+ (parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31) ||
35
+ (parts[0] === 192 && parts[1] === 168));
36
+ }
37
+ if (isIP(host) !== 6)
38
+ return false;
39
+ return (host === '::' ||
40
+ host === '::1' ||
41
+ host.startsWith('fc') ||
42
+ host.startsWith('fd') ||
43
+ /^fe[89ab]/u.test(host) ||
44
+ /^::ffff:(?:0|10|127|169\.254|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./u.test(host));
45
+ }
46
+ import { isIP } from 'node:net';
@@ -0,0 +1 @@
1
+ export declare function allocatePort(host?: string): Promise<number>;
@@ -0,0 +1,12 @@
1
+ import { createServer } from 'node:net';
2
+ export function allocatePort(host = '127.0.0.1') {
3
+ return new Promise((resolve, reject) => {
4
+ const server = createServer();
5
+ server.once('error', reject);
6
+ server.listen(0, host, () => {
7
+ const address = server.address();
8
+ const port = typeof address === 'object' && address !== null ? address.port : 0;
9
+ server.close((cause) => (cause === undefined ? resolve(port) : reject(cause)));
10
+ });
11
+ });
12
+ }
@@ -0,0 +1,14 @@
1
+ import type { Release } from '@astrale-os/sdk/deployment/release';
2
+ import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
3
+ import { resolvedHttpsFetch } from './resolved-fetch.js';
4
+ interface ReadinessDependencies {
5
+ readonly verify: typeof verifyReadiness;
6
+ readonly resolvedFetch: typeof resolvedHttpsFetch;
7
+ }
8
+ /** Verify the exact Release over both public identity and its loopback transport. */
9
+ export declare function verifyDevelopmentRelease(input: {
10
+ readonly release: Release;
11
+ readonly localUrl: string;
12
+ readonly signal: AbortSignal;
13
+ }, dependencies?: ReadinessDependencies): Promise<void>;
14
+ export {};
@@ -0,0 +1,71 @@
1
+ import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
2
+ import { resolvedHttpsFetch } from './resolved-fetch.js';
3
+ const defaults = Object.freeze({
4
+ verify: verifyReadiness,
5
+ resolvedFetch: resolvedHttpsFetch,
6
+ });
7
+ /** Verify the exact Release over both public identity and its loopback transport. */
8
+ export async function verifyDevelopmentRelease(input, dependencies = defaults) {
9
+ const sameOrigin = new URL(input.release.addressing.issuer).origin === new URL(input.localUrl).origin;
10
+ if (!sameOrigin) {
11
+ await retry(() => dependencies.verify(input.release, {
12
+ signal: attemptSignal(input.signal),
13
+ fetch: (url, init) => {
14
+ const requested = new URL(url);
15
+ const local = new URL(input.localUrl);
16
+ local.pathname = requested.pathname;
17
+ local.search = requested.search;
18
+ return fetch(local.href, init);
19
+ },
20
+ }), input.signal);
21
+ }
22
+ const publicHost = new URL(input.release.addressing.issuer).hostname;
23
+ const publicFetch = publicHost.endsWith('.trycloudflare.com')
24
+ ? dependencies.resolvedFetch(input.release.addressing.issuer)
25
+ : undefined;
26
+ await retry(() => dependencies.verify(input.release, {
27
+ signal: attemptSignal(input.signal),
28
+ ...(publicFetch === undefined ? {} : { fetch: publicFetch }),
29
+ }), input.signal);
30
+ }
31
+ async function retry(operation, signal) {
32
+ const deadline = Date.now() + 90_000;
33
+ let last;
34
+ while (Date.now() < deadline) {
35
+ throwIfAborted(signal);
36
+ try {
37
+ await operation();
38
+ return;
39
+ }
40
+ catch (cause) {
41
+ throwIfAborted(signal);
42
+ last = cause;
43
+ await pause(signal);
44
+ }
45
+ }
46
+ throw last instanceof Error
47
+ ? last
48
+ : new Error('Cloudflare development Release did not become ready.');
49
+ }
50
+ function attemptSignal(signal) {
51
+ return AbortSignal.any([signal, AbortSignal.timeout(5_000)]);
52
+ }
53
+ function pause(signal) {
54
+ return new Promise((resolve, reject) => {
55
+ const timer = setTimeout(done, 250);
56
+ const abort = () => done(signal.reason instanceof Error ? signal.reason : new Error('Development interrupted.'));
57
+ signal.addEventListener('abort', abort, { once: true });
58
+ function done(cause) {
59
+ clearTimeout(timer);
60
+ signal.removeEventListener('abort', abort);
61
+ if (cause === undefined)
62
+ resolve();
63
+ else
64
+ reject(cause);
65
+ }
66
+ });
67
+ }
68
+ function throwIfAborted(signal) {
69
+ if (signal.aborted)
70
+ throw signal.reason instanceof Error ? signal.reason : new Error('Development interrupted.');
71
+ }
@@ -0,0 +1,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 {};