@astrale-os/adapter-cloudflare 0.5.0-beta.35 → 0.5.0-beta.37

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.
@@ -57,7 +57,7 @@ async function prepare(params, context, direct) {
57
57
  main: `./${RELEASE_ENTRY}`,
58
58
  vars: runtimeVars(params, configuredIssuer),
59
59
  secretBindings: [SIGNING_IDENTITY_BINDING],
60
- modules: { baseDir: '.', globs: [RELEASE_SERVICE] },
60
+ modules: { baseDir: './bundle', globs: ['service.mjs'] },
61
61
  ...(context.build.frontend?.source.kind === 'vite'
62
62
  ? {
63
63
  frontend: {
@@ -121,6 +121,7 @@ async function prepare(params, context, direct) {
121
121
  }),
122
122
  notFound: 'none',
123
123
  runWorkerFirst: true,
124
+ htmlHandling: 'none',
124
125
  },
125
126
  }),
126
127
  });
@@ -31,6 +31,7 @@ export function generateWranglerConfig(opts) {
31
31
  config.assets = {
32
32
  directory: opts.frontend.directory,
33
33
  binding: 'ASSETS',
34
+ html_handling: 'none',
34
35
  run_worker_first: opts.frontend.dev
35
36
  ? true
36
37
  : [
@@ -0,0 +1,2 @@
1
+ import type { CloudflareParams } from '../../params.js';
2
+ export declare function workerRequestLimit(value: CloudflareParams['maximumRequestBytes']): number | undefined;
@@ -0,0 +1,9 @@
1
+ const MAXIMUM_REQUEST_BYTES = 16 * 1024 * 1024;
2
+ export function workerRequestLimit(value) {
3
+ if (value === undefined)
4
+ return undefined;
5
+ if (!Number.isSafeInteger(value) || value <= 0 || value > MAXIMUM_REQUEST_BYTES) {
6
+ throw new TypeError('Cloudflare maximumRequestBytes must be a positive integer at most 16 MiB.');
7
+ }
8
+ return value;
9
+ }
@@ -1,3 +1,4 @@
1
1
  export { deploymentAddressing } from './configuration/addressing.js';
2
+ export { workerRequestLimit } from './configuration/request-limit.js';
2
3
  export { resolveRouter } from './configuration/router.js';
3
4
  export { workerName } from './configuration/worker-name.js';
@@ -1,3 +1,4 @@
1
1
  export { deploymentAddressing } from './configuration/addressing.js';
2
+ export { workerRequestLimit } from './configuration/request-limit.js';
2
3
  export { resolveRouter } from './configuration/router.js';
3
4
  export { workerName } from './configuration/worker-name.js';
@@ -1,30 +1,70 @@
1
+ import { posix } from 'node:path';
1
2
  import { isDeepStrictEqual } from 'node:util';
2
3
  import { releaseNamespace, releaseWorkerName } from '../release/index.js';
3
- import { workerName } from './configuration.js';
4
+ import { generateWranglerConfig } from './codegen/wrangler.js';
5
+ import { resolveRouter, workerName } from './configuration.js';
6
+ import { frontendRoutes } from './frontend/routes.js';
4
7
  export function admitDirectPlacement(artifact, worker, parameters) {
5
8
  const name = workerName(parameters, worker.origin);
6
9
  const release = config(artifact, 'wrangler.release.jsonc');
7
10
  const gateway = config(artifact, 'wrangler.gateway.jsonc');
8
- equal(release.name, releaseWorkerName(worker.revision));
9
- equal(release.main, `./${worker.release.entrypoint}`);
11
+ const directory = posix.dirname(worker.release.entrypoint);
12
+ const router = resolveRouter(parameters.router);
13
+ const frontend = artifact.build.frontend;
14
+ const hasViteFrontend = frontend?.source.kind === 'vite';
15
+ equal(worker.assets !== undefined, hasViteFrontend);
16
+ if (worker.assets !== undefined) {
17
+ equal(worker.assets.notFound, 'none');
18
+ equal(worker.assets.runWorkerFirst, true);
19
+ }
20
+ equal(release, generated({
21
+ workerName: releaseWorkerName(worker.revision),
22
+ main: `./${worker.release.entrypoint}`,
23
+ vars: worker.bindings.vars,
24
+ secretBindings: worker.bindings.requiredSecrets,
25
+ modules: {
26
+ baseDir: `./${directory}`,
27
+ globs: moduleGlobs(worker, directory),
28
+ },
29
+ ...(hasViteFrontend
30
+ ? {
31
+ frontend: {
32
+ directory: './frontend',
33
+ routes: frontendRoutes(frontend),
34
+ dev: false,
35
+ },
36
+ }
37
+ : {}),
38
+ ...(router === undefined
39
+ ? {}
40
+ : { router: { binding: router.binding, service: router.service } }),
41
+ ...(parameters.wrangler === undefined ? {} : { wrangler: parameters.wrangler }),
42
+ selfBinding: false,
43
+ }));
10
44
  equal(release.compatibility_date, worker.runtime.compatibilityDate);
11
45
  equal(release.compatibility_flags, worker.runtime.compatibilityFlags);
12
- equal(release.vars ?? {}, worker.bindings.vars);
13
- equal(release.services ?? [], worker.bindings.services.map((binding) => ({
14
- binding: binding.name,
15
- service: binding.service,
16
- })));
17
- equal(record(release.secrets).required ?? [], worker.bindings.requiredSecrets);
18
- const rules = Array.isArray(release.rules) ? release.rules : [];
19
- equal(rules.flatMap((rule) => {
20
- const globs = record(rule).globs;
21
- return Array.isArray(globs) ? globs : [];
22
- }), worker.release.modules
46
+ equal(release.services ?? [], worker.bindings.services.map(serviceBinding));
47
+ equal(record(release.assets).html_handling, worker.assets?.htmlHandling);
48
+ equal(gateway, generated({
49
+ workerName: name,
50
+ main: `./${worker.gateway.entrypoint}`,
51
+ ...(parameters.route === undefined ? {} : { route: parameters.route }),
52
+ releases: { binding: 'RELEASES', namespace: releaseNamespace(name) },
53
+ selfBinding: false,
54
+ }));
55
+ }
56
+ function moduleGlobs(worker, directory) {
57
+ return worker.release.modules
23
58
  .filter((module) => module.path !== worker.release.entrypoint)
24
- .map((module) => module.path));
25
- equal(gateway.name, name);
26
- equal(gateway.main, `./${worker.gateway.entrypoint}`);
27
- equal(gateway.dispatch_namespaces, [{ binding: 'RELEASES', namespace: releaseNamespace(name) }]);
59
+ .map((module) => {
60
+ const relative = posix.relative(directory, module.path);
61
+ if (relative === '' || relative === '..' || relative.startsWith('../'))
62
+ mismatch();
63
+ return relative;
64
+ });
65
+ }
66
+ function generated(input) {
67
+ return JSON.parse(generateWranglerConfig(input));
28
68
  }
29
69
  function config(artifact, path) {
30
70
  const file = artifact.files.find((candidate) => candidate.path === path);
@@ -40,13 +80,18 @@ function config(artifact, path) {
40
80
  throw new TypeError(`Direct Cloudflare artifact ${path} is invalid.`, { cause });
41
81
  }
42
82
  }
83
+ function serviceBinding(binding) {
84
+ return { binding: binding.name, service: binding.service };
85
+ }
43
86
  function record(input) {
44
87
  return input !== null && typeof input === 'object' && !Array.isArray(input)
45
88
  ? input
46
89
  : {};
47
90
  }
48
91
  function equal(actual, expected) {
49
- if (!isDeepStrictEqual(actual, expected)) {
50
- throw new TypeError('Direct Cloudflare placement differs from its admitted Worker artifact.');
51
- }
92
+ if (!isDeepStrictEqual(actual, expected))
93
+ mismatch();
94
+ }
95
+ function mismatch() {
96
+ throw new TypeError('Direct Cloudflare placement differs from its admitted Worker artifact.');
52
97
  }
@@ -3,11 +3,12 @@ import { dirname, join } from 'node:path';
3
3
  import { admitWorkerBundle } from '../bundle/admit.js';
4
4
  import { generateWranglerConfig } from '../codegen/wrangler.js';
5
5
  import { qualifyWorkerBundle } from '../qualification.js';
6
+ import { workerModuleSpecifier } from '../worker/specifier.js';
6
7
  import { runWranglerBundle } from '../wrangler-cli.js';
7
8
  import { generateGatewayEntry } from './source.js';
8
9
  export async function prepareGateway(input) {
9
10
  const gatewaySource = join(input.directory, 'gateway.gen.ts');
10
- await writeFile(gatewaySource, generateGatewayEntry(input.revision));
11
+ await writeFile(gatewaySource, generateGatewayEntry(input.revision, workerModuleSpecifier()));
11
12
  const configPath = join(input.directory, 'wrangler.gateway.prepare.jsonc');
12
13
  await writeFile(configPath, generateWranglerConfig({
13
14
  workerName: input.name,
@@ -1 +1 @@
1
- export declare function generateGatewayEntry(revision: string): string;
1
+ export declare function generateGatewayEntry(revision: string, workerSpecifier?: string): string;
@@ -1,6 +1,6 @@
1
- export function generateGatewayEntry(revision) {
1
+ export function generateGatewayEntry(revision, workerSpecifier = '@astrale-os/adapter-cloudflare/worker') {
2
2
  return `// AUTO-GENERATED by @astrale-os/adapter-cloudflare — do not edit.
3
- import { cloudflareGatewayEntry } from '@astrale-os/adapter-cloudflare/worker'
3
+ import { cloudflareGatewayEntry } from ${JSON.stringify(workerSpecifier)}
4
4
 
5
5
  export default cloudflareGatewayEntry({ current: ${JSON.stringify(revision)} })
6
6
  `;
@@ -34,5 +34,6 @@ export interface WorkerArtifact {
34
34
  }[];
35
35
  readonly notFound: 'none' | '404-page' | 'single-page-application';
36
36
  readonly runWorkerFirst: boolean;
37
+ readonly htmlHandling?: 'none';
37
38
  };
38
39
  }
@@ -60,6 +60,9 @@ export function loadWorkerArtifact(artifact) {
60
60
  files: Object.freeze(assets),
61
61
  notFound: manifest.assets.notFound,
62
62
  runWorkerFirst: manifest.assets.runWorkerFirst,
63
+ ...(manifest.assets.htmlHandling === undefined
64
+ ? {}
65
+ : { htmlHandling: manifest.assets.htmlHandling }),
63
66
  }),
64
67
  }),
65
68
  });
@@ -175,10 +178,11 @@ function admitServices(input) {
175
178
  return Object.freeze(services);
176
179
  }
177
180
  function admitAssets(input) {
178
- const value = record(input, ['files', 'notFound', 'runWorkerFirst']);
181
+ const value = record(input, ['files', 'notFound', 'runWorkerFirst', 'htmlHandling']);
179
182
  if (!Array.isArray(value.files) ||
180
183
  !['none', '404-page', 'single-page-application'].includes(String(value.notFound)) ||
181
- typeof value.runWorkerFirst !== 'boolean')
184
+ typeof value.runWorkerFirst !== 'boolean' ||
185
+ (value.htmlHandling !== undefined && value.htmlHandling !== 'none'))
182
186
  invalid();
183
187
  const paths = new Set();
184
188
  const files = value.files.map((input) => {
@@ -206,6 +210,7 @@ function admitAssets(input) {
206
210
  files: Object.freeze(files),
207
211
  notFound: value.notFound,
208
212
  runWorkerFirst: value.runWorkerFirst,
213
+ ...(value.htmlHandling === undefined ? {} : { htmlHandling: 'none' }),
209
214
  });
210
215
  }
211
216
  function record(input, allowed) {
@@ -30,6 +30,7 @@ export interface WorkerArtifactManifestV1 {
30
30
  }[];
31
31
  readonly notFound: 'none' | '404-page' | 'single-page-application';
32
32
  readonly runWorkerFirst: boolean;
33
+ readonly htmlHandling?: 'none';
33
34
  };
34
35
  }
35
36
  interface ManifestExecutable {
@@ -34,6 +34,9 @@ export function encodeWorkerArtifactManifest(input) {
34
34
  .sort((a, b) => comparePath(a.path, b.path))),
35
35
  notFound: input.assets.notFound,
36
36
  runWorkerFirst: input.assets.runWorkerFirst,
37
+ ...(input.assets.htmlHandling === undefined
38
+ ? {}
39
+ : { htmlHandling: input.assets.htmlHandling }),
37
40
  },
38
41
  }),
39
42
  };
@@ -1,2 +1,2 @@
1
1
  /** Generate the isolate-startup entry that realizes the heavy Service on first request. */
2
- export declare function generateExecutionEntry(serviceSpecifier: string): string;
2
+ export declare function generateExecutionEntry(serviceSpecifier: string, workerSpecifier?: string): string;
@@ -1,7 +1,7 @@
1
1
  /** Generate the isolate-startup entry that realizes the heavy Service on first request. */
2
- export function generateExecutionEntry(serviceSpecifier) {
2
+ export function generateExecutionEntry(serviceSpecifier, workerSpecifier = '@astrale-os/adapter-cloudflare/worker') {
3
3
  return `// AUTO-GENERATED by @astrale-os/adapter-cloudflare — do not edit.
4
- import type { CloudflareWorker } from '@astrale-os/adapter-cloudflare/worker'
4
+ import type { CloudflareWorker } from ${JSON.stringify(workerSpecifier)}
5
5
 
6
6
  const SERVICE_SPECIFIER = ${JSON.stringify(serviceSpecifier)}
7
7
  let servicePromise: Promise<CloudflareWorker<CloudflareEnv>> | undefined
@@ -2,7 +2,9 @@ import type { BuildMaterial } from '@astrale-os/sdk/deployment/build';
2
2
  export interface ExecutionCodegenOptions {
3
3
  readonly origin: string;
4
4
  readonly runtimeSpecifier: string;
5
+ readonly workerSpecifier?: string;
5
6
  readonly material: BuildMaterial;
7
+ readonly maximumRequestBytes?: number;
6
8
  readonly providerAssignedWorker?: string;
7
9
  readonly router?: {
8
10
  readonly binding: string;
@@ -1,4 +1,5 @@
1
1
  export function generateExecutionService(options) {
2
+ const workerSpecifier = options.workerSpecifier ?? '@astrale-os/adapter-cloudflare/worker';
2
3
  const router = options.router;
3
4
  const routerPredicate = router === undefined
4
5
  ? ''
@@ -19,10 +20,10 @@ function isInstanceHost(host: string): boolean {
19
20
  return `// AUTO-GENERATED by @astrale-os/adapter-cloudflare — do not edit.
20
21
  // Domain origin: ${options.origin}
21
22
  import type { BuildMaterial } from '@astrale-os/sdk/deployment/build'
22
- import type { CloudflareAdapterBindings } from '@astrale-os/adapter-cloudflare/worker'
23
+ import type { CloudflareAdapterBindings } from ${JSON.stringify(workerSpecifier)}
23
24
 
24
- import { cloudflareWorkerEntry } from '@astrale-os/adapter-cloudflare/worker'
25
- import { decodeSigningIdentity, runtimeBindings } from '@astrale-os/adapter-cloudflare/worker'
25
+ import { cloudflareWorkerEntry } from ${JSON.stringify(workerSpecifier)}
26
+ import { decodeSigningIdentity, runtimeBindings } from ${JSON.stringify(workerSpecifier)}
26
27
 
27
28
  type WorkerEnv = CloudflareEnv & CloudflareAdapterBindings & {
28
29
  readonly ASTRALE_SIGNING_IDENTITY: string
@@ -37,6 +38,7 @@ export default cloudflareWorkerEntry<WorkerEnv, RuntimeEnv>()({
37
38
  privateKey: (env) => decodeSigningIdentity(env.ASTRALE_SIGNING_IDENTITY),
38
39
  resolveEnvironment: runtimeBindings,
39
40
  resolveUrl: (env, requestOrigin) => env.WORKER_URL ?? env.IDENTITY_ISS ?? requestOrigin,
41
+ ${options.maximumRequestBytes === undefined ? '' : `maximumRequestBytes: ${options.maximumRequestBytes},`}
40
42
  ${options.providerAssignedWorker === undefined ? '' : `providerAssignedWorker: ${JSON.stringify(options.providerAssignedWorker)},`}
41
43
  selfBinding: (env) => env.SELF,${routeSubrequest}
42
44
  })
@@ -5,17 +5,23 @@ import { SIGNING_IDENTITY_BINDING } from '../../identity/binding.js';
5
5
  import { admitWorkerBundle } from '../bundle/admit.js';
6
6
  import { generateExecutionEntry, generateExecutionService } from '../codegen/execution.js';
7
7
  import { generateWranglerConfig } from '../codegen/wrangler.js';
8
+ import { workerRequestLimit } from '../configuration.js';
8
9
  import { qualifyWorkerBundle } from '../qualification.js';
9
10
  import { runWranglerBundle } from '../wrangler-cli.js';
11
+ import { workerModuleSpecifier } from './specifier.js';
10
12
  export const RELEASE_ENTRY = 'bundle/index.mjs';
11
13
  export const RELEASE_SERVICE = 'bundle/service.mjs';
12
14
  export async function prepareExecution(input) {
13
15
  const runtimePath = resolve(input.context.projectDir, input.context.runtime.reference.path);
14
16
  const runtimeSpecifier = moduleSpecifier(relative(input.directory, runtimePath));
17
+ const workerSpecifier = workerModuleSpecifier();
18
+ const maximumRequestBytes = workerRequestLimit(input.params.maximumRequestBytes);
15
19
  const serviceSource = generateExecutionService({
16
20
  origin: input.context.build.schema.compiled.root.origin,
17
21
  runtimeSpecifier,
22
+ workerSpecifier,
18
23
  material: material(input.context.build),
24
+ ...(maximumRequestBytes === undefined ? {} : { maximumRequestBytes }),
19
25
  ...(input.configuredIssuer === undefined && input.direct
20
26
  ? { providerAssignedWorker: input.name }
21
27
  : {}),
@@ -59,7 +65,7 @@ export async function prepareExecution(input) {
59
65
  await mkdir(dirname(join(input.directory, RELEASE_SERVICE)), { recursive: true });
60
66
  await writeFile(join(input.directory, RELEASE_SERVICE), service);
61
67
  const sourcePath = join(input.directory, 'worker.gen.ts');
62
- await writeFile(sourcePath, generateExecutionEntry(`./${RELEASE_SERVICE}`));
68
+ await writeFile(sourcePath, generateExecutionEntry('./service.mjs', workerSpecifier));
63
69
  const configPath = join(input.directory, 'wrangler.prepare.jsonc');
64
70
  await writeFile(configPath, generateWranglerConfig({
65
71
  workerName: input.name,
@@ -0,0 +1 @@
1
+ export declare function workerModuleSpecifier(): string;
@@ -0,0 +1,5 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ const workerModule = import.meta.resolve('@astrale-os/adapter-cloudflare/worker');
3
+ export function workerModuleSpecifier() {
4
+ return fileURLToPath(workerModule);
5
+ }
package/dist/params.d.ts CHANGED
@@ -50,6 +50,12 @@ export interface CloudflareParams {
50
50
  readonly remote?: boolean;
51
51
  /** Extra plain (non-secret) vars to inject as Worker vars. */
52
52
  readonly vars?: Record<string, string>;
53
+ /**
54
+ * Maximum encoded invocation request bytes retained by the Worker Server.
55
+ * Defaults to 1 MiB and is capped at 16 MiB. Increase only from measured
56
+ * application traffic; Publication and Bundle delivery have separate limits.
57
+ */
58
+ readonly maximumRequestBytes?: number;
53
59
  /**
54
60
  * Escape hatch: raw wrangler config deep-merged over the generated base. Use
55
61
  * it to declare extra bindings the adapter has no typed field for — KV, R2,
@@ -26,6 +26,7 @@ export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings e
26
26
  readonly selfBinding?: (bindings: Bindings) => Fetcher | null | undefined;
27
27
  readonly routeSubrequest?: (url: URL, bindings: Bindings) => Fetcher | null | undefined;
28
28
  readonly invocationTimeoutMs?: number;
29
+ readonly maximumRequestBytes?: number;
29
30
  }
30
31
  export interface CloudflareWorkerSource<RuntimeValue extends Runtime> {
31
32
  readonly runtime: RuntimeValue;
@@ -42,7 +42,7 @@ export function cloudflareWorkerEntry() {
42
42
  server: {
43
43
  health: { probe: { check: () => true } },
44
44
  limits: {
45
- maximumRequestBytes: 1024 * 1024,
45
+ maximumRequestBytes: config.maximumRequestBytes ?? 1024 * 1024,
46
46
  maximumPublicationBytes: 1024 * 1024,
47
47
  maximumBundleBytes: 8 * 1024 * 1024,
48
48
  maximumDeliveryBytes: 8 * 1024 * 1024,
@@ -106,21 +106,22 @@ async function frontendAsset(frontend, environment, url, request) {
106
106
  if (development !== undefined && development.length > 0) {
107
107
  return fetch(new Request(new URL(`${url.pathname}${url.search}`, `${development}/`), request));
108
108
  }
109
- const route = frontend.routes
110
- .filter((candidate) => candidate.path === url.pathname ||
111
- (frontend.source.kind === 'vite' &&
112
- frontend.source.spa &&
113
- (candidate.path === '/' || url.pathname.startsWith(`${candidate.path}/`))))
114
- .sort((left, right) => right.path.length - left.path.length)[0];
115
- if (route === undefined && !url.pathname.startsWith('/assets/'))
109
+ const assetPath = url.pathname.startsWith('/assets/');
110
+ const route = assetPath
111
+ ? undefined
112
+ : frontend.routes
113
+ .filter((candidate) => candidate.path === url.pathname ||
114
+ (frontend.source.kind === 'vite' &&
115
+ frontend.source.spa &&
116
+ (candidate.path === '/' || url.pathname.startsWith(`${candidate.path}/`))))
117
+ .sort((left, right) => right.path.length - left.path.length)[0];
118
+ if (route === undefined && !assetPath)
116
119
  return undefined;
117
120
  const assets = environment.ASSETS;
118
121
  if (assets === undefined)
119
122
  return undefined;
120
- const response = await assets.fetch(request);
121
- if (response.status !== 404 || route === undefined)
122
- return response;
123
- await response.body?.cancel();
123
+ if (route === undefined)
124
+ return assets.fetch(request);
124
125
  return assets.fetch(new Request(new URL(`/${route.document}${url.search}`, url.origin), request));
125
126
  }
126
127
  function reserved(path) {
@@ -22,5 +22,5 @@ export function cloudflareGatewayEntry(config) {
22
22
  }
23
23
  /** Admit only Cloudflare's documented missing-script failure at the provider boundary. */
24
24
  function isMissingWorker(cause) {
25
- return cause instanceof Error && /^Worker not found(?:: [^\r\n]+)?$/iu.test(cause.message);
25
+ return cause instanceof Error && /^Worker not found(?:: [^\r\n]+|\.)?$/u.test(cause.message);
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/adapter-cloudflare",
3
- "version": "0.5.0-beta.35",
3
+ "version": "0.5.0-beta.37",
4
4
  "description": "Deploy an Astrale application through Cloudflare Workers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -45,7 +45,7 @@
45
45
  "hono": "^4.6.20",
46
46
  "jose": "^6.1.3",
47
47
  "wrangler": "4.123.0",
48
- "@astrale-os/sdk": "^0.5.0-beta.32"
48
+ "@astrale-os/sdk": "^0.5.0-beta.34"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@astrale-os/ox": ">=0.1.0 <1.0.0",