@astrale-os/adapter-cloudflare 0.5.0-beta.164 → 0.5.0-beta.166

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 (63) hide show
  1. package/README.md +64 -5
  2. package/dist/adapter.d.ts +5 -4
  3. package/dist/adapter.js +2 -0
  4. package/dist/build/artifact.js +134 -149
  5. package/dist/build/bundle/admit.d.ts +2 -0
  6. package/dist/build/bundle/admit.js +37 -0
  7. package/dist/build/bundle/compiled-wasm-loader.d.ts +8 -0
  8. package/dist/build/bundle/compiled-wasm-loader.js +21 -0
  9. package/dist/build/bundler.d.ts +21 -0
  10. package/dist/build/bundler.js +96 -0
  11. package/dist/build/codegen/wrangler.d.ts +4 -3
  12. package/dist/build/codegen/wrangler.js +35 -1
  13. package/dist/build/configuration/invocation-timeout.js +2 -2
  14. package/dist/build/configuration/legacy/direct.d.ts +15 -0
  15. package/dist/build/configuration/legacy/direct.js +19 -0
  16. package/dist/build/configuration/request-limit.js +1 -1
  17. package/dist/build/configuration/settings.d.ts +15 -0
  18. package/dist/build/configuration/settings.js +71 -0
  19. package/dist/build/configuration.d.ts +1 -0
  20. package/dist/build/configuration.js +1 -0
  21. package/dist/build/frontend/readiness.d.ts +2 -2
  22. package/dist/build/index.d.ts +3 -1
  23. package/dist/build/index.js +3 -1
  24. package/dist/build/worker/codegen/entry.d.ts +0 -1
  25. package/dist/build/worker/codegen/entry.js +0 -3
  26. package/dist/build/worker/codegen/service.d.ts +4 -11
  27. package/dist/build/worker/codegen/service.js +23 -28
  28. package/dist/build/worker/execution.prepare.d.ts +11 -11
  29. package/dist/build/worker/execution.prepare.js +69 -74
  30. package/dist/build/worker/modules.d.ts +8 -2
  31. package/dist/build/worker/modules.js +8 -2
  32. package/dist/build/worker/specifier.d.ts +2 -0
  33. package/dist/build/worker/specifier.js +5 -0
  34. package/dist/build/worker/upload.d.ts +18 -0
  35. package/dist/build/worker/upload.js +40 -0
  36. package/dist/deploy/deploy.d.ts +2 -2
  37. package/dist/deploy/deploy.js +18 -4
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +1 -1
  40. package/dist/params.d.ts +19 -3
  41. package/dist/release/build.d.ts +35 -0
  42. package/dist/release/build.js +44 -0
  43. package/dist/release/index.d.ts +1 -0
  44. package/dist/release/index.js +1 -0
  45. package/dist/worker/durable.d.ts +11 -0
  46. package/dist/worker/durable.js +24 -0
  47. package/dist/worker/entry.d.ts +12 -7
  48. package/dist/worker/entry.js +79 -56
  49. package/dist/worker/front.d.ts +23 -1
  50. package/dist/worker/front.js +3 -14
  51. package/dist/worker/frontend.d.ts +1 -1
  52. package/dist/worker/frontend.js +2 -2
  53. package/dist/worker/index.d.ts +3 -0
  54. package/dist/worker/index.js +3 -0
  55. package/dist/worker/legacy/direct.d.ts +37 -0
  56. package/dist/worker/legacy/direct.js +58 -0
  57. package/dist/worker/restate.d.ts +6 -0
  58. package/dist/worker/restate.js +6 -0
  59. package/dist/worker/settings.d.ts +48 -0
  60. package/dist/worker/settings.js +161 -0
  61. package/dist/wrangler/bundle.js +0 -1
  62. package/package.json +9 -4
  63. package/template/astrale.config.ts +2 -2
package/README.md CHANGED
@@ -16,10 +16,14 @@ as `deploy`; it starts no local Worker, Vite server, or tunnel. Omitting `route`
16
16
  stable `*.workers.dev` issuer. Add `installation: { instance: '...' }` to the Project Environment
17
17
  when generic tooling should reconcile the ready Release onto an Astrale Kernel.
18
18
 
19
+ Generated Worker modules are minified by default, including separately loaded runtime modules.
20
+ The Environment's `wrangler` overlay cannot change minification: every Environment of one source
21
+ must emit the same JavaScript bytes. `cloudflare({ wrangler: { minify: false } })` is rejected.
22
+
19
23
  One watcher serializes its own updates. For an existing Worker, it uploads a candidate, reconciles
20
24
  configured triggers, and stages the candidate at 0% while the previous version retains 100% of normal
21
25
  HTTP traffic. Exact-version probes verify Publication, Bundle, and serving identity before promotion;
22
- serving the Publication also initializes the Application Runtime. After promotion, normal requests
26
+ serving the Publication also initializes the Domain Runtime. After promotion, normal requests
23
27
  must prove that exact version again. A failed candidate remains at 0% and can be replaced on retry.
24
28
  Bundled View routes must also serve the prepared browser-file generation before readiness passes.
25
29
  This covers frontend-only changes without changing the Schema or reinstalling the Domain. HTML is
@@ -40,7 +44,7 @@ protects the HTTP implementation, not an atomic rollback of external resources o
40
44
  When an Environment also declares `installation`, the SDK excludes another local checkout that
41
45
  targets the same resolved `Kernel issuer + Domain origin` (including bookmark aliases), reconciles
42
46
  exact installed Release evidence, and opens
43
- the default View when the Application has a frontend. These are Project-tooling behaviors rather
47
+ the default View when the Domain has a frontend. These are Project-tooling behaviors rather
44
48
  than Cloudflare adapter responsibilities.
45
49
 
46
50
  ## Frontend files
@@ -76,6 +80,58 @@ candidate. The public Worker name and `/invoke` endpoint remain stable across co
76
80
  implementation and Schema updates. Provider staging identity never enters Release, Publication,
77
81
  Kernel routing, or graph identity.
78
82
 
83
+ ## Deployment bindings
84
+
85
+ The bundled Worker carries no Environment value, so every Environment of one source bundles the
86
+ same bytes. Each deployment binds its own values as plain Worker bindings:
87
+
88
+ | Binding | Value |
89
+ | --- | --- |
90
+ | `WORKER_URL` | serving origin of the deployment |
91
+ | `ASTRALE_SETTINGS` | canonical JSON of `{ invocationTimeoutMs, maximumRequestBytes, router, tokenExchange }`, defaults resolved, with optional `durableOptions` |
92
+ | `ASTRALE_BUILD_DIGEST` | build digest, bound only by hosts that deploy immutable Workers; never by a stable Worker |
93
+ | `IDENTITY_ISS`, `ASTRALE_PROVIDER_ASSIGNED_WORKER` | **legacy direct mode only** (deprecated): a stable issuer behind another route, and the workers.dev claim |
94
+
95
+ These names are adapter-owned: preparation refuses an author var, or a `wrangler.vars` entry, that
96
+ takes one, and deployment refuses a secret that does. A host that deploys the Worker itself encodes
97
+ the settings with `encodeWorkerSettings` from `@astrale-os/adapter-cloudflare/worker`. A generated
98
+ Worker refuses execution without `ASTRALE_SETTINGS`.
99
+
100
+ The two direct-mode bindings are deprecated. Their replacement is an immutable deployment, whose
101
+ host binds `WORKER_URL` to the deployment URL, which is also its issuer (namespace mode). They are
102
+ deleted with direct mode, once the Shell, Services, Issues, domains and ui configurations deploy in
103
+ namespace mode and no installation pins a direct Worker.
104
+
105
+ The `wrangler` overlay reaches the deployment configuration, never the bundle. Preparation refuses
106
+ an overlay key the bundler reads (`alias`, `build`, `define`, `jsx_factory`, `jsx_fragment`,
107
+ `keep_names`, `minify`, `no_bundle`, `node_compat`, `preserve_file_names`, `rules`, `tsconfig`,
108
+ `upload_source_maps`) and a compatibility flag it reads (Node.js, `navigator`, fetch iterable
109
+ typing, Python) beyond the adapter's own `nodejs_compat`.
110
+
111
+ ## Build digest
112
+
113
+ `cloudflare()` and `astrale()` bundle through one bundler, `cloudflare-worker` version `1`, so
114
+ every Environment of either adapter shares one build: `astrale-domain build` prints its build
115
+ digest, and `deploy` places that same Bundle. Two installed versions of this package are two
116
+ copies of the bundler: when their code differs, `build` prints one build digest per copy and warns
117
+ until the adapter versions are aligned.
118
+
119
+ Services receives the placed modules as they are, so there the digest names the bytes the
120
+ deployment runs. Direct mode runs `wrangler deploy`, which bundles the placed modules again: there
121
+ the digest names the portable Worker the deployment was made from, not the bytes Cloudflare runs.
122
+ A host that receives a Worker upload recomputes the digest from the exact upload with the same
123
+ function:
124
+
125
+ ```ts
126
+ import { workerBuildManifest } from '@astrale-os/adapter-cloudflare/release'
127
+ import { buildDigest } from '@astrale-os/sdk/deployment/build/manifest'
128
+
129
+ const digest = buildDigest(workerBuildManifest({ origin, revision, bundle, entry, modules, assets }))
130
+ ```
131
+
132
+ Modules keep their uploaded name and an asset `/<path>` is the file `assets/<path>`. The bundler
133
+ version names this layout, not a package release. Both subpaths are runtime-safe.
134
+
79
135
  ## Material for a custom Worker entrypoint
80
136
 
81
137
  Workers with provider-specific routes or environment initialization can reuse the adapter's
@@ -85,16 +141,19 @@ material emission without adopting the entire generated entrypoint:
85
141
  import { generateBuildMaterialModule } from '@astrale-os/adapter-cloudflare/build'
86
142
  import { writeFile } from 'node:fs/promises'
87
143
 
88
- // build is an admitted Build returned by compile(application).
144
+ // build is an admitted Build returned by compile(domain).
89
145
  await writeFile('src/build-material.gen.ts', generateBuildMaterialModule(build))
90
146
  ```
91
147
 
92
148
  The generated TypeScript module exports `loadBuildMaterial()`. Call it from the
93
- `load` callback of `generatedCloudflareWorkerEntry` alongside the application's Runtime.
149
+ `load` callback of `generatedCloudflareWorkerEntry` alongside the Domain's Runtime.
150
+ Such an entrypoint states its execution settings (`invocationTimeoutMs`, `maximumRequestBytes`,
151
+ `tokenExchange`, `routeSubrequest`, `identityIssuer`) in its configuration, or reads them from its
152
+ deployment bindings with `settings: deploymentSettings`; one Worker never takes both.
94
153
  It constructs the same verified, deduplicated material as the adapter's generated Service;
95
154
  allocation is deferred until the factory is called and the SDK deep-freezes it before use.
96
155
  The generated file needs only a type import from the SDK. Keep its compiler and runtime SDK
97
- versions aligned, and regenerate it when either the Application or that SDK version changes.
156
+ versions aligned, and regenerate it when either the Domain or that SDK version changes.
98
157
 
99
158
  Such an entrypoint can keep the same fast frontend path by answering from
100
159
  `cloudflareFrontDoor()` (`@astrale-os/adapter-cloudflare/worker/front`, which reaches no SDK
package/dist/adapter.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Deployment } from '@astrale-os/sdk/deployment';
2
- import type { Release } from '@astrale-os/sdk/deployment/release';
2
+ import type { LegacyAdapter } from '@astrale-os/sdk/deployment/adapter';
3
+ import type { legacy } from '@astrale-os/sdk/deployment/release';
3
4
  import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
4
5
  import type { CloudflareParams } from './params.js';
5
6
  import { artifact } from './build/artifact.js';
@@ -9,6 +10,6 @@ export interface CloudflareDependencies extends CloudflareDeployDependencies {
9
10
  readonly artifact: typeof artifact;
10
11
  readonly signingIdentity: typeof signingIdentity;
11
12
  }
12
- export declare function cloudflare(parameters?: CloudflareParams): Deployment<CloudflareParams>;
13
- export declare function cloudflareWithDependencies(parameters: CloudflareParams, dependencies: CloudflareDependencies): Deployment<CloudflareParams>;
14
- export declare function waitForReadiness(verify: typeof verifyReadiness, release: Release, signal?: AbortSignal): Promise<void>;
13
+ export declare function cloudflare(parameters?: CloudflareParams): Deployment<CloudflareParams, LegacyAdapter<CloudflareParams>>;
14
+ export declare function cloudflareWithDependencies(parameters: CloudflareParams, dependencies: CloudflareDependencies): Deployment<CloudflareParams, LegacyAdapter<CloudflareParams>>;
15
+ export declare function waitForReadiness(verify: typeof verifyReadiness, release: legacy.Release, signal?: AbortSignal): Promise<void>;
package/dist/adapter.js CHANGED
@@ -2,6 +2,7 @@ import { defineAdapter, defineDeployment } from '@astrale-os/sdk/deployment/adap
2
2
  import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
3
3
  import { createRequire } from 'node:module';
4
4
  import { artifact, materialize } from './build/artifact.js';
5
+ import { cloudflareWorkerBundler } from './build/bundler.js';
5
6
  import { verifyWorkerFrontend } from './build/frontend/readiness.js';
6
7
  import { deployCloudflare } from './deploy/index.js';
7
8
  import { versionReadinessFetch } from './deploy/readiness.js';
@@ -31,6 +32,7 @@ export function cloudflareWithDependencies(parameters, dependencies) {
31
32
  const adapter = defineAdapter({
32
33
  name: 'cloudflare',
33
34
  version: packageVersion,
35
+ bundler: cloudflareWorkerBundler,
34
36
  prepare: dependencies.artifact,
35
37
  deploy: (parameters, context) => deployCloudflare(parameters, context, dependencies),
36
38
  secretsFile: (parameters) => parameters.secrets,
@@ -1,15 +1,14 @@
1
1
  import { addressing } from '@astrale-os/sdk/deployment/release';
2
- import { mkdir, mkdtemp, rm } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
2
  import { SIGNING_IDENTITY_BINDING } from '../identity/binding.js';
5
3
  import { workerDeployment } from '../release/index.js';
4
+ import { bundledWorker, bundleWorker } from './bundler.js';
6
5
  import { generateWranglerConfig } from './codegen/wrangler.js';
7
- import { deploymentAddressing, resolveRouter, workerName } from './configuration.js';
8
- import { frontendFiles } from './frontend/files.js';
9
- import { frontendRevision } from './frontend/revision.js';
6
+ import { deploymentAddressing, deploymentVars, resolveRouter, workerName } from './configuration.js';
10
7
  import { encodeWorkerArtifactManifest, inferMediaType, workerContentDigest, WORKER_MANIFEST_PATH, } from './worker/artifact.manifest.js';
11
- import { prepareExecution, RELEASE_ENTRY, RELEASE_SERVICE, runtimeVars, } from './worker/execution.prepare.js';
8
+ import { RELEASE_ENTRY } from './worker/execution.prepare.js';
9
+ import { workerModules } from './worker/modules.js';
12
10
  import { admitWorkerRuntimeConfiguration } from './worker/runtime.js';
11
+ import { recordPlacedBundle } from './worker/upload.js';
13
12
  export { materialize } from './materialize.js';
14
13
  const encoder = new TextEncoder();
15
14
  /** Prepare one portable Worker whose public provider address remains stable across builds. */
@@ -21,150 +20,136 @@ export async function prepareWorkerArtifact(params, context) {
21
20
  return prepare(params, context, false);
22
21
  }
23
22
  async function prepare(params, context, direct) {
24
- const stateDirectory = join(context.projectDir, '.astrale');
25
- await mkdir(stateDirectory, { recursive: true });
26
- const directory = await mkdtemp(join(stateDirectory, 'prepare-'));
27
- try {
28
- const router = resolveRouter(params.router);
29
- const name = workerName(params, context.build.schema.compiled.root.origin, context.environment);
30
- const revision = context.build.schema.compiled.root.revision;
31
- const configuredIssuer = deploymentAddressing(params);
32
- // `frontend` stays the served file list every branch below already reads; `addressed` rides
33
- // alongside it into the Worker, where it decides which of those files a browser may retain.
34
- const { files: frontend, addressed } = await frontendFiles({
35
- frontend: context.build.frontend,
36
- projectDir: context.projectDir,
37
- output: join(directory, 'frontend'),
38
- recordSource: context.recordSource,
39
- signal: context.signal,
40
- });
41
- const modules = await prepareExecution({
42
- params,
43
- context,
44
- directory,
45
- name,
46
- direct,
47
- configuredIssuer,
48
- router,
49
- ...(frontend.length === 0 ? {} : { frontendRevision: frontendRevision(frontend) }),
50
- ...(addressed.length === 0 ? {} : { frontendAddressed: addressed }),
51
- });
52
- const releaseConfig = generateWranglerConfig({
53
- workerName: name,
54
- main: `./${RELEASE_ENTRY}`,
55
- ...(params.route === undefined ? {} : { route: params.route }),
56
- vars: runtimeVars(params, configuredIssuer),
57
- secretBindings: [SIGNING_IDENTITY_BINDING],
58
- modules: { baseDir: './bundle', rules: [{ type: 'ESModule', globs: ['service.mjs'] }] },
59
- ...(context.build.frontend?.source.kind === 'vite'
60
- ? {
61
- frontend: {
62
- directory: './frontend',
63
- },
64
- }
65
- : {}),
66
- ...(router === undefined
67
- ? {}
68
- : { router: { binding: router.binding, service: router.service } }),
69
- ...(params.wrangler === undefined ? {} : { wrangler: params.wrangler }),
70
- selfBinding: false,
71
- });
72
- const runtime = admitWorkerRuntimeConfiguration(releaseConfig);
73
- const deployment = workerDeployment({
74
- release: {
75
- entrypoint: RELEASE_ENTRY,
76
- modules: [
77
- { path: RELEASE_ENTRY, kind: 'esm', content: modules.entry },
78
- { path: RELEASE_SERVICE, kind: 'esm', content: modules.service },
79
- ],
80
- },
81
- runtime,
82
- bindings: {
83
- vars: runtime.vars,
84
- services: runtime.services,
85
- requiredSecrets: [SIGNING_IDENTITY_BINDING],
86
- },
87
- ...(frontend.length === 0
88
- ? {}
89
- : {
90
- assets: {
91
- files: frontend.map((file) => {
92
- const contentType = file.mediaType ?? inferMediaType(file.path);
93
- return {
94
- path: `/${file.path}`,
95
- content: file.bytes(),
96
- ...(contentType === undefined ? {} : { contentType }),
97
- };
98
- }),
99
- notFound: 'none',
100
- runWorkerFirst: true,
101
- htmlHandling: 'none',
102
- },
103
- }),
104
- });
105
- const manifest = encodeWorkerArtifactManifest({
106
- origin: context.build.schema.compiled.root.origin,
107
- revision,
108
- deployment,
109
- release: {
110
- entrypoint: RELEASE_ENTRY,
111
- modules: [
112
- { path: RELEASE_ENTRY, kind: 'esm', digest: workerContentDigest(modules.entry) },
113
- { path: RELEASE_SERVICE, kind: 'esm', digest: workerContentDigest(modules.service) },
114
- ],
115
- },
116
- runtime: {
117
- compatibilityDate: runtime.compatibilityDate,
118
- compatibilityFlags: runtime.compatibilityFlags,
119
- },
120
- bindings: {
121
- vars: runtime.vars,
122
- services: runtime.services,
123
- requiredSecrets: [SIGNING_IDENTITY_BINDING],
124
- },
125
- ...(frontend.length === 0
126
- ? {}
127
- : {
128
- assets: {
129
- files: frontend.map((file) => {
130
- const contentType = file.mediaType ?? inferMediaType(file.path);
131
- return {
132
- path: `/${file.path}`,
133
- artifactPath: `frontend/${file.path}`,
134
- digest: workerContentDigest(file.bytes()),
135
- ...(contentType === undefined ? {} : { contentType }),
136
- };
137
- }),
138
- notFound: 'none',
139
- runWorkerFirst: true,
140
- htmlHandling: 'none',
141
- },
142
- }),
143
- });
144
- const files = [
145
- { path: RELEASE_ENTRY, content: modules.entry },
146
- { path: RELEASE_SERVICE, content: modules.service },
147
- { path: WORKER_MANIFEST_PATH, content: manifest },
148
- ...frontend.map((file) => ({
149
- path: `frontend/${file.path}`,
150
- content: file.bytes(),
23
+ const router = resolveRouter(params.router);
24
+ const name = workerName(params, context.build.schema.compiled.root.origin, context.environment);
25
+ const revision = context.build.schema.compiled.root.revision;
26
+ const configuredIssuer = deploymentAddressing(params);
27
+ // The Environment enters only as the deployment's bindings, never the bundled bytes.
28
+ const vars = deploymentVars(params, {
29
+ ...(configuredIssuer === undefined ? {} : { configuredIssuer }),
30
+ ...(direct && configuredIssuer === undefined ? { providerAssignedWorker: name } : {}),
31
+ });
32
+ // Preparation places the environment-free Worker bytes: the Bundle `deploy` already produced
33
+ // with this adapter's bundler, or the same bundling run here.
34
+ const worker = context.bundle === undefined
35
+ ? await bundleWorker(context)
36
+ : bundledWorker(context.bundle, context.build);
37
+ if (context.bundle !== undefined)
38
+ recordPlacedBundle(context.build);
39
+ const { frontend } = worker;
40
+ const release = {
41
+ entrypoint: RELEASE_ENTRY,
42
+ modules: [
43
+ { path: RELEASE_ENTRY, kind: 'esm', content: worker.entry },
44
+ ...worker.modules,
45
+ ],
46
+ };
47
+ const releaseConfig = generateWranglerConfig({
48
+ workerName: name,
49
+ main: `./${RELEASE_ENTRY}`,
50
+ ...(params.route === undefined ? {} : { route: params.route }),
51
+ vars,
52
+ secretBindings: [SIGNING_IDENTITY_BINDING],
53
+ modules: workerModules(release),
54
+ ...(context.build.frontend?.source.kind === 'vite'
55
+ ? {
56
+ frontend: {
57
+ directory: './frontend',
58
+ },
59
+ }
60
+ : {}),
61
+ ...(router === undefined
62
+ ? {}
63
+ : { router: { binding: router.binding, service: router.service } }),
64
+ ...(params.wrangler === undefined ? {} : { wrangler: params.wrangler }),
65
+ selfBinding: false,
66
+ });
67
+ const runtime = admitWorkerRuntimeConfiguration(releaseConfig);
68
+ const deployment = workerDeployment({
69
+ release,
70
+ runtime,
71
+ bindings: {
72
+ vars: runtime.vars,
73
+ services: runtime.services,
74
+ requiredSecrets: [SIGNING_IDENTITY_BINDING],
75
+ },
76
+ ...(frontend.length === 0
77
+ ? {}
78
+ : {
79
+ assets: {
80
+ files: frontend.map((file) => {
81
+ const contentType = file.mediaType ?? inferMediaType(file.path);
82
+ return {
83
+ path: `/${file.path}`,
84
+ content: file.bytes(),
85
+ ...(contentType === undefined ? {} : { contentType }),
86
+ };
87
+ }),
88
+ notFound: 'none',
89
+ runWorkerFirst: true,
90
+ htmlHandling: 'none',
91
+ },
92
+ }),
93
+ });
94
+ const manifest = encodeWorkerArtifactManifest({
95
+ origin: context.build.schema.compiled.root.origin,
96
+ revision,
97
+ deployment,
98
+ release: {
99
+ entrypoint: release.entrypoint,
100
+ modules: release.modules.map(({ path, kind, content }) => ({
101
+ path,
102
+ kind,
103
+ digest: workerContentDigest(content),
151
104
  })),
152
- ...(direct ? [{ path: 'wrangler.jsonc', content: encoder.encode(releaseConfig) }] : []),
153
- ];
154
- return Object.freeze({
155
- entrypoint: RELEASE_ENTRY,
156
- files: Object.freeze(files),
157
- addressing: configuredIssuer === undefined
158
- ? Object.freeze({ kind: 'provider-assigned', claim: name })
159
- : Object.freeze({
160
- kind: 'configured',
161
- addressing: addressing(params.identityIssuer ?? configuredIssuer, {
162
- publicUrl: configuredIssuer,
105
+ },
106
+ runtime: {
107
+ compatibilityDate: runtime.compatibilityDate,
108
+ compatibilityFlags: runtime.compatibilityFlags,
109
+ },
110
+ bindings: {
111
+ vars: runtime.vars,
112
+ services: runtime.services,
113
+ requiredSecrets: [SIGNING_IDENTITY_BINDING],
114
+ },
115
+ ...(frontend.length === 0
116
+ ? {}
117
+ : {
118
+ assets: {
119
+ files: frontend.map((file) => {
120
+ const contentType = file.mediaType ?? inferMediaType(file.path);
121
+ return {
122
+ path: `/${file.path}`,
123
+ artifactPath: `frontend/${file.path}`,
124
+ digest: workerContentDigest(file.bytes()),
125
+ ...(contentType === undefined ? {} : { contentType }),
126
+ };
163
127
  }),
128
+ notFound: 'none',
129
+ runWorkerFirst: true,
130
+ htmlHandling: 'none',
131
+ },
132
+ }),
133
+ });
134
+ const files = [
135
+ ...release.modules.map(({ path, content }) => ({ path, content })),
136
+ { path: WORKER_MANIFEST_PATH, content: manifest },
137
+ ...frontend.map((file) => ({
138
+ path: `frontend/${file.path}`,
139
+ content: file.bytes(),
140
+ })),
141
+ ...(direct ? [{ path: 'wrangler.jsonc', content: encoder.encode(releaseConfig) }] : []),
142
+ ];
143
+ return Object.freeze({
144
+ entrypoint: RELEASE_ENTRY,
145
+ files: Object.freeze(files),
146
+ addressing: configuredIssuer === undefined
147
+ ? Object.freeze({ kind: 'provider-assigned', claim: name })
148
+ : Object.freeze({
149
+ kind: 'configured',
150
+ addressing: addressing(params.identityIssuer ?? configuredIssuer, {
151
+ publicUrl: configuredIssuer,
164
152
  }),
165
- });
166
- }
167
- finally {
168
- await rm(directory, { recursive: true, force: true });
169
- }
153
+ }),
154
+ });
170
155
  }
@@ -1 +1,3 @@
1
1
  export declare function admitWorkerBundle(path: string): Promise<void>;
2
+ /** Admit the separately emitted engine's actual platform exports, including its compiled WASM. */
3
+ export declare function admitDurableEngineBundle(path: string): Promise<void>;
@@ -1,5 +1,34 @@
1
+ import * as nodeModule from 'node:module';
1
2
  import { pathToFileURL } from 'node:url';
3
+ import { compiledWasmLoader, compiledWasmSource } from './compiled-wasm-loader.js';
4
+ let compiledWasm = false;
5
+ /**
6
+ * Import a WebAssembly module as Workers do for `CompiledWasm`: its default export is the compiled
7
+ * `WebAssembly.Module`, instantiated by the importing code. The CLI admits bundles under Bun; Node
8
+ * serves programmatic builds.
9
+ */
10
+ function admitCompiledWasm() {
11
+ if (compiledWasm)
12
+ return;
13
+ if (process.versions.bun !== undefined) {
14
+ ;
15
+ Reflect.get(globalThis, 'Bun').plugin({
16
+ name: 'astrale-compiled-wasm',
17
+ setup(build) {
18
+ build.onLoad({ filter: /\.wasm$/ }, ({ path }) => ({
19
+ loader: 'js',
20
+ contents: compiledWasmSource(JSON.stringify(path)),
21
+ }));
22
+ },
23
+ });
24
+ compiledWasm = true;
25
+ return;
26
+ }
27
+ nodeModule.register(compiledWasmLoader);
28
+ compiledWasm = true;
29
+ }
2
30
  export async function admitWorkerBundle(path) {
31
+ admitCompiledWasm();
3
32
  const loaded = (await import(`${pathToFileURL(path).href}?admit=${Date.now()}`));
4
33
  if (loaded.default === null ||
5
34
  typeof loaded.default !== 'object' ||
@@ -7,3 +36,11 @@ export async function admitWorkerBundle(path) {
7
36
  throw new TypeError('Generated Worker bundle does not export a Fetch handler.');
8
37
  }
9
38
  }
39
+ /** Admit the separately emitted engine's actual platform exports, including its compiled WASM. */
40
+ export async function admitDurableEngineBundle(path) {
41
+ admitCompiledWasm();
42
+ const engine = (await import(`${pathToFileURL(path).href}?admit=${Date.now()}`));
43
+ if (['createEndpointHandler', 'workflow', 'TerminalError', 'CancelledError'].some((key) => typeof engine[key] !== 'function')) {
44
+ throw new TypeError('Generated durable engine bundle does not export the platform Fetch entrypoint.');
45
+ }
46
+ }
@@ -0,0 +1,8 @@
1
+ /** Workers expose CompiledWasm imports as modules, rather than instantiated exports. */
2
+ export declare function compiledWasmSource(file: string): string;
3
+ /**
4
+ * Async hooks run in a separate Node thread. A self-contained ESM hook admits both emitted JS and
5
+ * source builds under tsx without requiring that thread to resolve or transpile our source files.
6
+ * The emitted import source is shared with Bun.
7
+ */
8
+ export declare const compiledWasmLoader: string;
@@ -0,0 +1,21 @@
1
+ const SOURCE_PREFIX = "import { readFileSync } from 'node:fs'\nexport default new WebAssembly.Module(readFileSync(";
2
+ const SOURCE_SUFFIX = '))\n';
3
+ /** Workers expose CompiledWasm imports as modules, rather than instantiated exports. */
4
+ export function compiledWasmSource(file) {
5
+ return `${SOURCE_PREFIX}${file}${SOURCE_SUFFIX}`;
6
+ }
7
+ /**
8
+ * Async hooks run in a separate Node thread. A self-contained ESM hook admits both emitted JS and
9
+ * source builds under tsx without requiring that thread to resolve or transpile our source files.
10
+ * The emitted import source is shared with Bun.
11
+ */
12
+ export const compiledWasmLoader = `data:text/javascript,${encodeURIComponent(`
13
+ export function load(url, context, nextLoad) {
14
+ if (!url.startsWith('file:') || !url.endsWith('.wasm')) return nextLoad(url, context);
15
+ return {
16
+ format: 'module',
17
+ shortCircuit: true,
18
+ source: ${JSON.stringify(SOURCE_PREFIX)} + 'new URL(' + JSON.stringify(url) + ')' + ${JSON.stringify(SOURCE_SUFFIX)},
19
+ };
20
+ }
21
+ `)}`;
@@ -0,0 +1,21 @@
1
+ import type { Bundle, BundleContext, Bundler } from '@astrale-os/sdk/deployment/adapter';
2
+ import type { frontend as frontendBuild } from '@astrale-os/sdk/deployment/build';
3
+ import type { ReleaseModule } from './worker/execution.prepare.js';
4
+ /**
5
+ * Environment-free bytes of one generated Worker: its entry, the modules it loads (the execution
6
+ * Service, the deferred workflow engine and their WASM) and its frontend.
7
+ */
8
+ export interface WorkerBytes {
9
+ readonly entry: Uint8Array;
10
+ readonly modules: readonly ReleaseModule[];
11
+ readonly frontend: readonly frontendBuild.FrontendFile[];
12
+ }
13
+ /**
14
+ * Bundles every generated Cloudflare Worker, for adapter-cloudflare and adapter-astrale alike. It
15
+ * reads no Environment: settings, address, issuer and key are bound at deploy.
16
+ */
17
+ export declare const cloudflareWorkerBundler: Bundler;
18
+ /** Bundle the Worker modules and build the frontend they serve, in one scratch directory. */
19
+ export declare function bundleWorker(context: BundleContext): Promise<WorkerBytes>;
20
+ /** Read back the Worker bytes of a Bundle this bundler produced for the same Build. */
21
+ export declare function bundledWorker(bundle: Bundle, build: Bundle['build']): WorkerBytes;
@@ -0,0 +1,96 @@
1
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { CLOUDFLARE_WORKER_BUNDLER, workerAssetFile } from '../release/build.js';
4
+ import { frontendFiles } from './frontend/files.js';
5
+ import { frontendRevision } from './frontend/revision.js';
6
+ import { prepareExecution, RELEASE_ENTRY, RELEASE_SERVICE } from './worker/execution.prepare.js';
7
+ /** Worker modules live beside the entry; WASM is the only non-ESM kind preparation emits. */
8
+ const MODULE_DIRECTORY = 'bundle/';
9
+ /**
10
+ * Bundles every generated Cloudflare Worker, for adapter-cloudflare and adapter-astrale alike. It
11
+ * reads no Environment: settings, address, issuer and key are bound at deploy.
12
+ */
13
+ export const cloudflareWorkerBundler = Object.freeze({
14
+ id: CLOUDFLARE_WORKER_BUNDLER.id,
15
+ version: CLOUDFLARE_WORKER_BUNDLER.version,
16
+ async bundle(context) {
17
+ const worker = await bundleWorker(context);
18
+ return Object.freeze({
19
+ entrypoint: RELEASE_ENTRY,
20
+ files: Object.freeze([
21
+ { path: RELEASE_ENTRY, content: worker.entry },
22
+ ...worker.modules.map(({ path, content }) => ({ path, content })),
23
+ ...worker.frontend.map((file) => ({
24
+ path: workerAssetFile(`/${file.path}`),
25
+ content: file.bytes(),
26
+ })),
27
+ ]),
28
+ });
29
+ },
30
+ });
31
+ /** Bundle the Worker modules and build the frontend they serve, in one scratch directory. */
32
+ export async function bundleWorker(context) {
33
+ const stateDirectory = join(context.projectDir, '.astrale');
34
+ await mkdir(stateDirectory, { recursive: true });
35
+ const directory = await mkdtemp(join(stateDirectory, 'prepare-'));
36
+ try {
37
+ // `frontend` is the served file list; `addressed` rides alongside it into the Worker, where it
38
+ // decides which of those files a browser may retain.
39
+ const { files: frontend, addressed } = await frontendFiles({
40
+ frontend: context.build.frontend,
41
+ projectDir: context.projectDir,
42
+ output: join(directory, 'frontend'),
43
+ recordSource: context.recordSource,
44
+ signal: context.signal,
45
+ });
46
+ const modules = await prepareExecution({
47
+ context,
48
+ directory,
49
+ ...(frontend.length === 0 ? {} : { frontendRevision: frontendRevision(frontend) }),
50
+ ...(addressed.length === 0 ? {} : { frontendAddressed: addressed }),
51
+ });
52
+ return Object.freeze({ entry: modules.entry, modules: modules.modules, frontend });
53
+ }
54
+ finally {
55
+ await rm(directory, { recursive: true, force: true });
56
+ }
57
+ }
58
+ /** Read back the Worker bytes of a Bundle this bundler produced for the same Build. */
59
+ export function bundledWorker(bundle, build) {
60
+ const { id, version } = bundle.manifest.bundler;
61
+ if (bundle.build !== build ||
62
+ id !== CLOUDFLARE_WORKER_BUNDLER.id ||
63
+ version !== CLOUDFLARE_WORKER_BUNDLER.version ||
64
+ bundle.entrypoint !== RELEASE_ENTRY) {
65
+ throw new TypeError('Cloudflare preparation requires the Bundle of its Worker bundler.');
66
+ }
67
+ let entry;
68
+ const modules = [];
69
+ const frontend = [];
70
+ for (const file of bundle.files) {
71
+ if (file.path === RELEASE_ENTRY)
72
+ entry = file.bytes();
73
+ else if (file.path.startsWith(MODULE_DIRECTORY) && /\.(?:mjs|wasm)$/u.test(file.path)) {
74
+ modules.push(Object.freeze({
75
+ path: file.path,
76
+ kind: file.path.endsWith('.wasm') ? 'wasm' : 'esm',
77
+ content: file.bytes(),
78
+ }));
79
+ }
80
+ else if (file.path.startsWith('assets/')) {
81
+ const content = file.bytes();
82
+ frontend.push(Object.freeze({ path: file.path.slice('assets/'.length), bytes: () => content.slice() }));
83
+ }
84
+ else {
85
+ throw new TypeError(`Cloudflare Worker Bundle file ${file.path} is not a Worker file.`);
86
+ }
87
+ }
88
+ if (entry === undefined || !modules.some((module) => module.path === RELEASE_SERVICE)) {
89
+ throw new TypeError('Cloudflare Worker Bundle lacks its execution modules.');
90
+ }
91
+ return Object.freeze({
92
+ entry,
93
+ modules: Object.freeze(modules),
94
+ frontend: Object.freeze(frontend),
95
+ });
96
+ }