@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
@@ -5,7 +5,10 @@ const TYPES = Object.freeze({
5
5
  text: 'Text',
6
6
  data: 'Data',
7
7
  });
8
- /** Project one admitted Worker module inventory into Wrangler's relative module discovery. */
8
+ /**
9
+ * Project one admitted Worker module inventory into Wrangler's relative module discovery. Rules
10
+ * follow module kinds, so the projection does not depend on the inventory's order.
11
+ */
9
12
  export function workerModules(worker) {
10
13
  const directory = posix.dirname(worker.entrypoint);
11
14
  const groups = new Map();
@@ -21,6 +24,9 @@ export function workerModules(worker) {
21
24
  globs.push(relative);
22
25
  groups.set(type, globs);
23
26
  }
24
- const rules = [...groups].map(([type, globs]) => Object.freeze({ type, globs: Object.freeze(globs) }));
27
+ const rules = Object.values(TYPES).flatMap((type) => {
28
+ const globs = groups.get(type);
29
+ return globs === undefined ? [] : [Object.freeze({ type, globs: Object.freeze(globs) })];
30
+ });
25
31
  return Object.freeze({ baseDir: `./${directory}`, rules: Object.freeze(rules) });
26
32
  }
@@ -1,3 +1,5 @@
1
1
  export declare function workerModuleSpecifier(): string;
2
2
  /** The isolate-startup module, which must stay free of the execution Service's dependencies. */
3
3
  export declare function frontModuleSpecifier(): string;
4
+ /** The engine's Workers build, resolved from this adapter so a Domain needs no engine dependency. */
5
+ export declare function restateModuleSpecifier(): string;
@@ -1,5 +1,6 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  const workerModule = import.meta.resolve('@astrale-os/adapter-cloudflare/worker');
3
+ const restateModule = import.meta.resolve('@astrale-os/adapter-cloudflare/worker/restate');
3
4
  const frontModule = import.meta.resolve('@astrale-os/adapter-cloudflare/worker/front');
4
5
  export function workerModuleSpecifier() {
5
6
  return fileURLToPath(workerModule);
@@ -8,3 +9,7 @@ export function workerModuleSpecifier() {
8
9
  export function frontModuleSpecifier() {
9
10
  return fileURLToPath(frontModule);
10
11
  }
12
+ /** The engine's Workers build, resolved from this adapter so a Domain needs no engine dependency. */
13
+ export function restateModuleSpecifier() {
14
+ return fileURLToPath(restateModule);
15
+ }
@@ -0,0 +1,18 @@
1
+ import type { PreparedArtifact } from '@astrale-os/sdk/deployment/adapter';
2
+ import type { Build } from '@astrale-os/sdk/deployment/build';
3
+ import type { BuildDigest } from '@astrale-os/sdk/deployment/build/manifest';
4
+ import type { WorkerArtifact } from './artifact.js';
5
+ import { type WorkerUpload } from '../../release/build.js';
6
+ /** The portable upload of one prepared Worker: exactly the modules and assets Services receives. */
7
+ export declare function workerUpload(artifact: PreparedArtifact, worker: WorkerArtifact): WorkerUpload;
8
+ /** Build digest of the portable upload of one prepared Worker. */
9
+ export declare function uploadBuildDigest(artifact: PreparedArtifact, worker: WorkerArtifact): BuildDigest;
10
+ /** Record that preparation placed the CLI's Bundle for this Build. */
11
+ export declare function recordPlacedBundle(build: Build): void;
12
+ /**
13
+ * The `build` field of one prepared Worker's provider evidence: the build digest of its portable
14
+ * upload, present only when its preparation placed the CLI's Bundle.
15
+ */
16
+ export declare function uploadBuildEvidence(artifact: PreparedArtifact, worker: WorkerArtifact): {
17
+ readonly build?: BuildDigest;
18
+ };
@@ -0,0 +1,40 @@
1
+ import { buildDigest } from '@astrale-os/sdk/deployment/build/manifest';
2
+ import { workerBuildManifest } from '../../release/build.js';
3
+ /** The portable upload of one prepared Worker: exactly the modules and assets Services receives. */
4
+ export function workerUpload(artifact, worker) {
5
+ const root = artifact.build.schema.compiled.root;
6
+ if (worker.origin !== root.origin || worker.revision !== root.revision) {
7
+ throw new TypeError('Worker artifact differs from its prepared Build.');
8
+ }
9
+ return Object.freeze({
10
+ origin: root.origin,
11
+ revision: root.revision,
12
+ bundle: artifact.build.schema.bundle.ref.digest,
13
+ entry: worker.release.entrypoint,
14
+ modules: worker.release.modules.map((module) => ({ name: module.path, bytes: module.bytes() })),
15
+ ...(worker.assets === undefined
16
+ ? {}
17
+ : { assets: worker.assets.files.map((file) => ({ path: file.path, bytes: file.bytes() })) }),
18
+ });
19
+ }
20
+ /** Build digest of the portable upload of one prepared Worker. */
21
+ export function uploadBuildDigest(artifact, worker) {
22
+ return buildDigest(workerBuildManifest(workerUpload(artifact, worker)));
23
+ }
24
+ /**
25
+ * Builds whose preparation placed the Bundle `astrale-domain deploy` passed (`PrepareContext.bundle`).
26
+ * Only a CLI that passes a Bundle reads `ProviderDeploymentEvidence.build`: an older one refuses an
27
+ * evidence field it does not know, after the provider has applied the upload.
28
+ */
29
+ const placedBuilds = new WeakSet();
30
+ /** Record that preparation placed the CLI's Bundle for this Build. */
31
+ export function recordPlacedBundle(build) {
32
+ placedBuilds.add(build);
33
+ }
34
+ /**
35
+ * The `build` field of one prepared Worker's provider evidence: the build digest of its portable
36
+ * upload, present only when its preparation placed the CLI's Bundle.
37
+ */
38
+ export function uploadBuildEvidence(artifact, worker) {
39
+ return placedBuilds.has(artifact.build) ? { build: uploadBuildDigest(artifact, worker) } : {};
40
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DeployContext, DeployResult } from '@astrale-os/sdk/deployment/adapter';
2
- import type { Release } from '@astrale-os/sdk/deployment/release';
2
+ import { legacy } from '@astrale-os/sdk/deployment/release';
3
3
  import type { materialize } from '../build/artifact.js';
4
4
  import type { WorkerArtifact } from '../build/worker/artifact.js';
5
5
  import type { signingIdentity } from '../identity/source.js';
@@ -10,7 +10,7 @@ export interface CloudflareDeployDependencies {
10
10
  readonly materialize: typeof materialize;
11
11
  readonly runWranglerRollout: typeof runWranglerRollout;
12
12
  readonly runWranglerDeploymentStatus: typeof runWranglerDeploymentStatus;
13
- readonly waitForReadiness: (release: Release, signal: AbortSignal, version: string, worker: WorkerArtifact, workerName?: string) => Promise<void>;
13
+ readonly waitForReadiness: (release: legacy.Release, signal: AbortSignal, version: string, worker: WorkerArtifact, workerName?: string) => Promise<void>;
14
14
  readonly signingIdentity: typeof signingIdentity;
15
15
  }
16
16
  /** Deploy one stable Worker target and verify its exact serving implementation. */
@@ -1,9 +1,10 @@
1
- import { addressing, assemble } from '@astrale-os/sdk/deployment/release';
1
+ import { addressing, legacy } from '@astrale-os/sdk/deployment/release';
2
2
  import { mkdir, mkdtemp, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { workerName } from '../build/configuration.js';
4
+ import { isAdapterVar, workerName } from '../build/configuration.js';
5
5
  import { admitDirectPlacement } from '../build/direct.js';
6
6
  import { loadWorkerArtifact } from '../build/worker/index.js';
7
+ import { uploadBuildEvidence } from '../build/worker/upload.js';
7
8
  import { signingIdentitySecrets } from '../identity/secret.js';
8
9
  import { WranglerDeployError } from '../wrangler/deploy.js';
9
10
  import { WranglerRolloutError } from '../wrangler/rollout.js';
@@ -24,8 +25,19 @@ export async function deployCloudflare(parameters, context, dependencies) {
24
25
  return failed('prerequisite', progress({}), cause);
25
26
  }
26
27
  let worker;
28
+ let build;
27
29
  try {
28
30
  worker = loadWorkerArtifact(context.artifact);
31
+ build = uploadBuildEvidence(context.artifact, worker);
32
+ const occupied = new Set([
33
+ ...Object.keys(worker.bindings.vars),
34
+ ...worker.bindings.services.map((binding) => binding.name),
35
+ ]);
36
+ for (const name of Object.keys(secrets)) {
37
+ if (occupied.has(name) || isAdapterVar(name)) {
38
+ throw new TypeError(`Cloudflare Worker secret ${name} collides with a binding.`);
39
+ }
40
+ }
29
41
  for (const name of worker.bindings.requiredSecrets) {
30
42
  if (!Object.hasOwn(secrets, name)) {
31
43
  throw new TypeError(`Cloudflare Worker requires secret ${name}.`);
@@ -51,7 +63,7 @@ export async function deployCloudflare(parameters, context, dependencies) {
51
63
  workerName: name,
52
64
  verifyCandidate: async (candidate) => {
53
65
  const selected = observeAddressing(context.artifact, candidate.url, name);
54
- const release = assemble(context.artifact.build, selected);
66
+ const release = legacy.assemble(context.artifact.build, selected);
55
67
  await dependencies.waitForReadiness(release, context.signal, candidate.deploymentVersion, worker, name);
56
68
  },
57
69
  secrets,
@@ -82,6 +94,7 @@ export async function deployCloudflare(parameters, context, dependencies) {
82
94
  provider: 'cloudflare',
83
95
  deployment: deployed.deploymentVersion,
84
96
  artifact: context.artifact.digest,
97
+ ...build,
85
98
  }),
86
99
  });
87
100
  let selected;
@@ -112,7 +125,7 @@ export async function deployCloudflare(parameters, context, dependencies) {
112
125
  }
113
126
  let release;
114
127
  try {
115
- release = assemble(context.artifact.build, selected);
128
+ release = legacy.assemble(context.artifact.build, selected);
116
129
  }
117
130
  catch (cause) {
118
131
  return failed('release', progress({ provider: retained, configuration }), cause);
@@ -129,6 +142,7 @@ export async function deployCloudflare(parameters, context, dependencies) {
129
142
  provider: 'cloudflare',
130
143
  deployment: deployed.deploymentVersion,
131
144
  artifact: context.artifact.digest,
145
+ ...build,
132
146
  issuer: selected.issuer,
133
147
  }),
134
148
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@astrale-os/adapter-cloudflare` — deploy an Astrale application through
2
+ * `@astrale-os/adapter-cloudflare` — deploy an Astrale Domain through
3
3
  * Cloudflare Workers on YOUR OWN Cloudflare account.
4
4
  *
5
5
  * import { cloudflare } from '@astrale-os/adapter-cloudflare'
@@ -19,4 +19,4 @@
19
19
  * tooling owns optional Kernel installation after provider readiness.
20
20
  */
21
21
  export { cloudflare } from './cloudflare.js';
22
- export type { CloudflareParams } from './params.js';
22
+ export type { CloudflareParams, DurableWorkerOptions } from './params.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@astrale-os/adapter-cloudflare` — deploy an Astrale application through
2
+ * `@astrale-os/adapter-cloudflare` — deploy an Astrale Domain through
3
3
  * Cloudflare Workers on YOUR OWN Cloudflare account.
4
4
  *
5
5
  * import { cloudflare } from '@astrale-os/adapter-cloudflare'
package/dist/params.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExchangeOptions } from '@astrale-os/sdk/execution';
1
+ import type { DurableServing, ExchangeOptions } from '@astrale-os/sdk/execution';
2
2
  /**
3
3
  * `CloudflareParams` — the provider-typed config for the Cloudflare adapter.
4
4
  *
@@ -50,6 +50,13 @@ export interface CloudflareParams {
50
50
  * The default maximum is 30 minutes; production and CI environments may select a shorter bound.
51
51
  */
52
52
  readonly tokenExchange?: ExchangeOptions | false;
53
+ /**
54
+ * What a Worker serving durable Workflows declares to the workflow engine, as `serve({ durable })`
55
+ * does: run retention, retry before pausing and the deadline of each Step. Canonical callbacks
56
+ * discover engine keys from their Kernel. The deprecated `ASTRALE_RESTATE_IDENTITY_KEYS` var
57
+ * admits retained legacy callbacks only.
58
+ */
59
+ readonly durable?: DurableWorkerOptions;
53
60
  /**
54
61
  * Escape hatch: raw wrangler config deep-merged over the generated base. Use
55
62
  * it to declare extra bindings the adapter has no typed field for — KV, R2,
@@ -67,6 +74,13 @@ export interface CloudflareParams {
67
74
  * `routes` are rejected — use `workerName` and `route` instead. Avoid
68
75
  * the reserved binding names `SELF` and `ASSETS`. Durable Objects need extra
69
76
  * worker-entry codegen and aren't supported through this field yet.
77
+ *
78
+ * The overlay reaches the deployment configuration, never the bundle: every
79
+ * Environment of one source bundles the same bytes. Keys the bundler reads
80
+ * (`alias`, `build`, `define`, `jsx_factory`, `jsx_fragment`, `keep_names`,
81
+ * `minify`, `no_bundle`, `node_compat`, `preserve_file_names`, `rules`,
82
+ * `tsconfig`, `upload_source_maps`) and compatibility flags it reads (Node.js,
83
+ * `navigator`, fetch iterable typing, Python) are rejected.
70
84
  */
71
85
  readonly wrangler?: Record<string, unknown>;
72
86
  /**
@@ -75,8 +89,9 @@ export interface CloudflareParams {
75
89
  * same-zone Worker calls honor public routes too.
76
90
  *
77
91
  * Wires BOTH halves the SDK needs: the `<binding>` service binding in the
78
- * generated wrangler config, and a `routeSubrequest` predicate in the worker
79
- * entry that diverts matching hosts through it.
92
+ * deployment's wrangler config, and the host suffix and label rule bound in
93
+ * `ASTRALE_SETTINGS`, which the Worker applies to divert matching hosts
94
+ * through that binding.
80
95
  *
81
96
  * Opt in only when the selected router owns every hostname matched by the
82
97
  * suffix/label predicate. A shared DNS suffix is not proof of router ownership:
@@ -96,3 +111,4 @@ export interface CloudflareParams {
96
111
  readonly minLabels?: number;
97
112
  };
98
113
  }
114
+ export type DurableWorkerOptions = Pick<DurableServing, 'retentionMs' | 'retry' | 'stepTimeoutMs'>;
@@ -0,0 +1,35 @@
1
+ import type { BuildManifestV1 } from '@astrale-os/sdk/deployment/build/manifest';
2
+ /**
3
+ * The bundler of every generated Cloudflare Worker, whichever adapter deploys it. Its version names
4
+ * the upload layout below, not a package release: a Services host recomputes the same build digest
5
+ * from the same bytes whatever SDK it runs.
6
+ */
7
+ export declare const CLOUDFLARE_WORKER_BUNDLER: Readonly<{
8
+ id: "cloudflare-worker";
9
+ version: "1";
10
+ }>;
11
+ /** Portable Worker upload: what Services receives, and what a dispatch namespace upload carries. */
12
+ export interface WorkerUpload {
13
+ readonly origin: string;
14
+ readonly revision: BuildManifestV1['schema']['revision'];
15
+ /** Digest of the Domain Schema bundle. */
16
+ readonly bundle: BuildManifestV1['schema']['bundle'];
17
+ readonly entry: string;
18
+ readonly modules: readonly {
19
+ readonly name: string;
20
+ readonly bytes: Uint8Array;
21
+ }[];
22
+ /** Static assets by their absolute Worker asset path (`/index.html`). */
23
+ readonly assets?: readonly {
24
+ readonly path: string;
25
+ readonly bytes: Uint8Array;
26
+ }[];
27
+ }
28
+ /**
29
+ * The one build manifest of a Worker upload, computed on the exact upload the deploy sends; the
30
+ * SDK, Services and namespace mode all identify Worker code through it. Modules keep their uploaded
31
+ * name; a static asset `/<path>` is the file `assets/<path>`.
32
+ */
33
+ export declare function workerBuildManifest(upload: WorkerUpload): BuildManifestV1;
34
+ /** The build-manifest file of one Worker asset path. */
35
+ export declare function workerAssetFile(path: string): string;
@@ -0,0 +1,44 @@
1
+ import { buildManifest } from '@astrale-os/sdk/deployment/build/manifest';
2
+ /**
3
+ * The bundler of every generated Cloudflare Worker, whichever adapter deploys it. Its version names
4
+ * the upload layout below, not a package release: a Services host recomputes the same build digest
5
+ * from the same bytes whatever SDK it runs.
6
+ */
7
+ export const CLOUDFLARE_WORKER_BUNDLER = Object.freeze({
8
+ id: 'cloudflare-worker',
9
+ version: '1',
10
+ });
11
+ /**
12
+ * The one build manifest of a Worker upload, computed on the exact upload the deploy sends; the
13
+ * SDK, Services and namespace mode all identify Worker code through it. Modules keep their uploaded
14
+ * name; a static asset `/<path>` is the file `assets/<path>`.
15
+ */
16
+ export function workerBuildManifest(upload) {
17
+ if (upload === null || typeof upload !== 'object' || !Array.isArray(upload.modules)) {
18
+ throw new TypeError('Worker upload is invalid.');
19
+ }
20
+ if (upload.assets !== undefined && !Array.isArray(upload.assets)) {
21
+ throw new TypeError('Worker upload assets are invalid.');
22
+ }
23
+ return buildManifest({
24
+ origin: upload.origin,
25
+ revision: upload.revision,
26
+ bundle: upload.bundle,
27
+ bundler: CLOUDFLARE_WORKER_BUNDLER,
28
+ entrypoint: upload.entry,
29
+ files: [
30
+ ...upload.modules.map((module) => ({ path: module.name, content: module.bytes })),
31
+ ...(upload.assets ?? []).map((asset) => ({
32
+ path: workerAssetFile(asset.path),
33
+ content: asset.bytes,
34
+ })),
35
+ ],
36
+ });
37
+ }
38
+ /** The build-manifest file of one Worker asset path. */
39
+ export function workerAssetFile(path) {
40
+ if (typeof path !== 'string' || !path.startsWith('/')) {
41
+ throw new TypeError('Worker asset path must be absolute.');
42
+ }
43
+ return `assets${path}`;
44
+ }
@@ -1,2 +1,3 @@
1
+ export { CLOUDFLARE_WORKER_BUNDLER, workerBuildManifest, type WorkerUpload } from './build.js';
1
2
  export { acceptWorkerDeployment, workerDeployment, workerDeploymentMaterial, type WorkerDeploymentId, type WorkerDeploymentInput, } from './deployment.js';
2
3
  export { stagedWorkerName } from './staged-worker-name.js';
@@ -1,2 +1,3 @@
1
+ export { CLOUDFLARE_WORKER_BUNDLER, workerBuildManifest } from './build.js';
1
2
  export { acceptWorkerDeployment, workerDeployment, workerDeploymentMaterial, } from './deployment.js';
2
3
  export { stagedWorkerName } from './staged-worker-name.js';
@@ -0,0 +1,11 @@
1
+ import type { DurableEngine, DurableServing } from '@astrale-os/sdk/execution';
2
+ /** @deprecated Static request identity keys for retained legacy callbacks only. */
3
+ export declare const RESTATE_IDENTITY_KEYS_BINDING = "ASTRALE_RESTATE_IDENTITY_KEYS";
4
+ export interface DurableBindings {
5
+ readonly [RESTATE_IDENTITY_KEYS_BINDING]?: string;
6
+ }
7
+ /**
8
+ * Canonical callbacks discover their engine's authority from the Kernel named in their path.
9
+ * The optional request preserves the three-argument legacy helper's static-key behavior.
10
+ */
11
+ export declare function restateServing(engine: DurableEngine, bindings: DurableBindings, options?: Pick<DurableServing, 'retentionMs' | 'retry' | 'stepTimeoutMs'>, request?: Request): DurableServing | undefined;
@@ -0,0 +1,24 @@
1
+ import { isCanonicalDurableRequest, isLegacyDurableRequest } from '@astrale-os/sdk/execution';
2
+ /** @deprecated Static request identity keys for retained legacy callbacks only. */
3
+ export const RESTATE_IDENTITY_KEYS_BINDING = 'ASTRALE_RESTATE_IDENTITY_KEYS';
4
+ /**
5
+ * Canonical callbacks discover their engine's authority from the Kernel named in their path.
6
+ * The optional request preserves the three-argument legacy helper's static-key behavior.
7
+ */
8
+ export function restateServing(engine, bindings, options = {}, request) {
9
+ const canonical = request !== undefined && isCanonicalDurableRequest(request);
10
+ if (request !== undefined && !canonical && !isLegacyDurableRequest(request))
11
+ return undefined;
12
+ const value = bindings[RESTATE_IDENTITY_KEYS_BINDING];
13
+ if (value === undefined || value.trim() === '') {
14
+ return canonical ? Object.freeze({ ...options, engine }) : undefined;
15
+ }
16
+ return Object.freeze({
17
+ ...options,
18
+ engine,
19
+ identityKeys: Object.freeze(value
20
+ .split(',')
21
+ .map((key) => key.trim())
22
+ .filter((key) => key.length > 0)),
23
+ });
24
+ }
@@ -1,20 +1,25 @@
1
1
  import type { JsonWebKey } from '@astrale-os/sdk/auth';
2
2
  import type { BuildMaterial, GeneratedBuildMaterial } from '@astrale-os/sdk/deployment/build/material';
3
- import type { ExchangeOptions, ExecutionContext } from '@astrale-os/sdk/execution';
3
+ import type { DurableServingProvider, ExecutionContext } from '@astrale-os/sdk/execution';
4
4
  import type { Runtime } from '@astrale-os/sdk/runtime';
5
+ import type { DurableWorkerOptions } from '../params.js';
5
6
  import type { CloudflareAdapterBindings, CloudflareFrontDoorConfig, Fetcher } from './front.js';
7
+ import type { CloudflareWorkerSettings } from './settings.js';
6
8
  export type { CloudflareAdapterBindings, Fetcher } from './front.js';
7
- export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings extends CloudflareAdapterBindings, Environment, Material = BuildMaterial> extends CloudflareFrontDoorConfig<Bindings> {
9
+ export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings extends CloudflareAdapterBindings, Environment, Material = BuildMaterial> extends CloudflareFrontDoorConfig<Bindings>, CloudflareWorkerSettings<Bindings> {
8
10
  /** Load the exact Runtime and Build once, after the isolate has accepted a request. */
9
11
  readonly load: () => CloudflareWorkerSource<RuntimeValue, Material> | Promise<CloudflareWorkerSource<RuntimeValue, Material>>;
10
12
  readonly privateKey: JsonWebKey | ((bindings: Bindings) => JsonWebKey);
11
13
  readonly resolveEnvironment?: (bindings: Bindings) => Environment;
12
- readonly identityIssuer?: string;
13
14
  readonly selfBinding?: (bindings: Bindings) => Fetcher | null | undefined;
14
- readonly routeSubrequest?: (url: URL, bindings: Bindings) => Fetcher | null | undefined;
15
- readonly invocationTimeoutMs?: number;
16
- readonly maximumRequestBytes?: number;
17
- readonly tokenExchange?: ExchangeOptions | false;
15
+ /**
16
+ * Read the execution settings from the deployment's bindings on the isolate's first execution
17
+ * request, as generated Workers do with `deploymentSettings`. A hand-composed Worker states
18
+ * them in this configuration instead; one Worker never takes both.
19
+ */
20
+ readonly settings?: (bindings: Bindings) => CloudflareWorkerSettings<Bindings>;
21
+ /** Resolve the engine for this durable request with the deployment's captured options. */
22
+ readonly durable?: (bindings: Bindings, request: Request, options?: DurableWorkerOptions) => ReturnType<DurableServingProvider>;
18
23
  }
19
24
  export interface CloudflareWorkerSource<RuntimeValue extends Runtime, Material = BuildMaterial> {
20
25
  readonly runtime: RuntimeValue;
@@ -1,10 +1,11 @@
1
1
  import { issuer } from '@astrale-os/sdk/auth';
2
2
  import { restoreBuild, restoreGeneratedBuild } from '@astrale-os/sdk/deployment/build/material';
3
- import { addressing, assemble } from '@astrale-os/sdk/deployment/release/runtime';
3
+ import { addressing, legacy } from '@astrale-os/sdk/deployment/release/runtime';
4
4
  import { createIdentity, serve } from '@astrale-os/sdk/execution';
5
5
  import { dispatchCloudflareRequest, prepareCloudflareRequest } from './fetch.js';
6
6
  import { cloudflareFrontDoor, withProviderVersion } from './front.js';
7
7
  import { frontendAsset } from './frontend.js';
8
+ import { DEFAULT_INVOCATION_TIMEOUT_MS, DEFAULT_MAXIMUM_REQUEST_BYTES } from './settings.js';
8
9
  /** Compose one Service implementation behind its provider-owned stable address. */
9
10
  export function cloudflareWorkerEntry() {
10
11
  return createCloudflareWorkerEntry(restoreBuild);
@@ -13,60 +14,82 @@ export function cloudflareWorkerEntry() {
13
14
  export function generatedCloudflareWorkerEntry() {
14
15
  return createCloudflareWorkerEntry(restoreGeneratedBuild);
15
16
  }
17
+ /** The static settings a hand-composed Worker may state instead of reading its bindings. */
18
+ const SETTINGS = [
19
+ 'identityIssuer',
20
+ 'routeSubrequest',
21
+ 'invocationTimeoutMs',
22
+ 'maximumRequestBytes',
23
+ 'tokenExchange',
24
+ 'durableOptions',
25
+ ];
16
26
  function createCloudflareWorkerEntry(restore) {
17
27
  return function (config) {
28
+ if (config.settings !== undefined && SETTINGS.some((key) => config[key] !== undefined)) {
29
+ throw new TypeError('Cloudflare Worker settings come from its deployment bindings or its configuration, not both.');
30
+ }
18
31
  const frontDoor = cloudflareFrontDoor(config);
19
- let realized;
20
- const realize = () => {
21
- if (realized !== undefined)
22
- return realized;
23
- realized = Promise.resolve()
24
- .then(config.load)
25
- .then((source) => {
26
- const build = restore(source.material, source.runtime);
27
- const application = serve({
28
- runtime: source.runtime,
29
- load(environment) {
30
- if (frontDoor.current() !== environment.publicUrl) {
31
- throw new TypeError('Cloudflare Worker request origin differs from its current Release.');
32
- }
33
- return assemble(build, addressing(config.identityIssuer ?? environment.publicUrl, {
34
- publicUrl: environment.publicUrl,
35
- }));
36
- },
37
- initialize: (environment) => config.resolveEnvironment === undefined
38
- ? environment.bindings
39
- : config.resolveEnvironment(environment.bindings),
40
- identity(environment) {
41
- return createIdentity({
42
- issuer: issuer.accept(config.identityIssuer ?? environment.publicUrl),
43
- subject: build.schema.compiled.root.origin,
44
- privateKey: typeof config.privateKey === 'function'
45
- ? config.privateKey(environment.bindings)
46
- : config.privateKey,
47
- fetch: routedFetch(application, environment, config),
48
- ...(new URL(environment.publicUrl).protocol === 'http:'
49
- ? { allowInsecureHttp: true }
50
- : {}),
51
- ...(config.tokenExchange === undefined
52
- ? {}
53
- : { tokenExchange: config.tokenExchange }),
54
- });
55
- },
56
- server: {
57
- health: { probe: { check: () => true } },
58
- limits: {
59
- maximumRequestBytes: config.maximumRequestBytes ?? 1024 * 1024,
60
- maximumPublicationBytes: 1024 * 1024,
61
- maximumBundleBytes: 8 * 1024 * 1024,
62
- maximumDeliveryBytes: 8 * 1024 * 1024,
63
- invocationTimeoutMs: config.invocationTimeoutMs ?? 30_000,
64
- },
32
+ let restored;
33
+ let application;
34
+ const realize = () => (restored ??= Promise.resolve()
35
+ .then(config.load)
36
+ .then((source) => Object.freeze({
37
+ runtime: source.runtime,
38
+ build: restore(source.material, source.runtime),
39
+ })));
40
+ // Settings are a deployment's, so they are read from the first execution request's bindings.
41
+ const served = (source, bindings) => {
42
+ if (application !== undefined)
43
+ return application;
44
+ const { build } = source;
45
+ const settings = config.settings === undefined ? config : config.settings(bindings);
46
+ const created = serve({
47
+ runtime: source.runtime,
48
+ load(environment) {
49
+ if (frontDoor.current() !== environment.publicUrl) {
50
+ throw new TypeError('Cloudflare Worker request origin differs from its current Release.');
51
+ }
52
+ return legacy.assemble(build, addressing(settings.identityIssuer ?? environment.publicUrl, {
53
+ publicUrl: environment.publicUrl,
54
+ }));
55
+ },
56
+ initialize: (environment) => config.resolveEnvironment === undefined
57
+ ? environment.bindings
58
+ : config.resolveEnvironment(environment.bindings),
59
+ identity(environment) {
60
+ return createIdentity({
61
+ issuer: issuer.accept(settings.identityIssuer ?? environment.publicUrl),
62
+ subject: build.schema.compiled.root.origin,
63
+ privateKey: typeof config.privateKey === 'function'
64
+ ? config.privateKey(environment.bindings)
65
+ : config.privateKey,
66
+ fetch: routedFetch(created, environment, config.selfBinding, settings.routeSubrequest),
67
+ ...(new URL(environment.publicUrl).protocol === 'http:'
68
+ ? { allowInsecureHttp: true }
69
+ : {}),
70
+ ...(settings.tokenExchange === undefined
71
+ ? {}
72
+ : { tokenExchange: settings.tokenExchange }),
73
+ });
74
+ },
75
+ ...(config.durable === undefined
76
+ ? {}
77
+ : {
78
+ durable: (environment, request) => config.durable(environment.bindings, request, settings.durableOptions),
79
+ }),
80
+ server: {
81
+ health: { probe: { check: () => true } },
82
+ limits: {
83
+ maximumRequestBytes: settings.maximumRequestBytes ?? DEFAULT_MAXIMUM_REQUEST_BYTES,
84
+ maximumPublicationBytes: 1024 * 1024,
85
+ maximumBundleBytes: 8 * 1024 * 1024,
86
+ maximumDeliveryBytes: 8 * 1024 * 1024,
87
+ invocationTimeoutMs: settings.invocationTimeoutMs ?? DEFAULT_INVOCATION_TIMEOUT_MS,
65
88
  },
66
- });
67
- return Object.freeze({ build, application });
89
+ },
68
90
  });
69
- return realized;
91
+ application = created;
92
+ return created;
70
93
  };
71
94
  return Object.freeze({
72
95
  async fetch(request, bindings, context) {
@@ -74,13 +97,13 @@ function createCloudflareWorkerEntry(restore) {
74
97
  if (admission.kind !== 'execution')
75
98
  return admission.response;
76
99
  const { url, publicUrl } = admission;
77
- const { application, build } = await realize();
100
+ const source = await realize();
78
101
  if (config.frontend === undefined) {
79
- const frontend = await frontendAsset(build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
102
+ const frontend = await frontendAsset(source.build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
80
103
  if (frontend !== undefined)
81
104
  return withProviderVersion(frontend, bindings);
82
105
  }
83
- const response = await application.fetch(request, Object.freeze({ bindings, publicUrl }), context);
106
+ const response = await served(source, bindings).fetch(request, Object.freeze({ bindings, publicUrl }), context);
84
107
  return withProviderVersion(response, bindings);
85
108
  },
86
109
  async prepare() {
@@ -89,17 +112,17 @@ function createCloudflareWorkerEntry(restore) {
89
112
  });
90
113
  };
91
114
  }
92
- function routedFetch(application, environment, config) {
115
+ function routedFetch(application, environment, selfBinding, routeSubrequest) {
93
116
  return async (input, init) => {
94
117
  const { request, rejectRedirect } = prepareCloudflareRequest(input, init);
95
118
  const destination = new URL(request.url);
96
119
  if (destination.origin === environment.publicUrl) {
97
- const self = config.selfBinding?.(environment.bindings);
120
+ const self = selfBinding?.(environment.bindings);
98
121
  return dispatchCloudflareRequest(request, rejectRedirect, (selected) => self === null || self === undefined
99
122
  ? application.fetch(selected, environment)
100
123
  : self.fetch(selected));
101
124
  }
102
- const route = config.routeSubrequest?.(destination, environment.bindings);
125
+ const route = routeSubrequest?.(destination, environment.bindings);
103
126
  return dispatchCloudflareRequest(request, rejectRedirect, (selected) => route === null || route === undefined ? fetch(selected) : route.fetch(selected));
104
127
  };
105
128
  }