@astrale-os/adapter-cloudflare 0.5.0-beta.158 → 0.5.0-beta.159
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.
- package/README.md +10 -2
- package/dist/build/worker/codegen/entry.d.ts +10 -1
- package/dist/build/worker/codegen/entry.js +42 -2
- package/dist/build/worker/execution.prepare.js +15 -2
- package/dist/build/worker/specifier.d.ts +2 -0
- package/dist/build/worker/specifier.js +5 -0
- package/dist/worker/entry.d.ts +5 -22
- package/dist/worker/entry.js +10 -64
- package/dist/worker/front.d.ts +49 -0
- package/dist/worker/front.js +75 -0
- package/dist/worker/frontend.d.ts +1 -1
- package/dist/worker/frontend.js +12 -4
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -22,8 +22,11 @@ HTTP traffic. Exact-version probes verify Publication, Bundle, and serving ident
|
|
|
22
22
|
serving the Publication also initializes the Application Runtime. After promotion, normal requests
|
|
23
23
|
must prove that exact version again. A failed candidate remains at 0% and can be replaced on retry.
|
|
24
24
|
Bundled View routes must also serve the prepared browser-file generation before readiness passes.
|
|
25
|
-
This covers frontend-only changes without changing the Schema or reinstalling the Domain. HTML
|
|
26
|
-
and transient asset misses are not cached; ordinary successful asset
|
|
25
|
+
This covers frontend-only changes without changing the Schema or reinstalling the Domain. HTML is
|
|
26
|
+
revalidated on every use and transient asset misses are not cached; ordinary successful asset
|
|
27
|
+
responses keep their cache policy. Frontend files are answered by the Worker's small startup module
|
|
28
|
+
without loading the execution Service, which each such file starts loading in the background for the
|
|
29
|
+
calls the View makes next.
|
|
27
30
|
The first deployment uses ordinary Worker provisioning because no previous version exists to retain.
|
|
28
31
|
An existing traffic-split rollout must be resolved before this single-version workflow can continue.
|
|
29
32
|
|
|
@@ -92,3 +95,8 @@ It constructs the same verified, deduplicated material as the adapter's generate
|
|
|
92
95
|
allocation is deferred until the factory is called and the SDK deep-freezes it before use.
|
|
93
96
|
The generated file needs only a type import from the SDK. Keep its compiler and runtime SDK
|
|
94
97
|
versions aligned, and regenerate it when either the Application or that SDK version changes.
|
|
98
|
+
|
|
99
|
+
Such an entrypoint can keep the same fast frontend path by answering from
|
|
100
|
+
`cloudflareFrontDoor()` (`@astrale-os/adapter-cloudflare/worker/front`, which reaches no SDK
|
|
101
|
+
runtime module) before importing its Service, and calling that Service's `prepare()` through
|
|
102
|
+
`waitUntil` when the front door served a frontend file.
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import type { BuildMaterial } from '@astrale-os/sdk/deployment/build';
|
|
2
|
+
export interface ExecutionEntryOptions {
|
|
3
|
+
readonly workerSpecifier?: string;
|
|
4
|
+
readonly frontSpecifier?: string;
|
|
5
|
+
readonly frontend?: BuildMaterial['frontend'];
|
|
6
|
+
readonly frontendRevision?: string;
|
|
7
|
+
readonly frontendAddressed?: readonly string[];
|
|
8
|
+
readonly providerAssignedWorker?: string;
|
|
9
|
+
}
|
|
1
10
|
/** Generate the isolate-startup entry that realizes the heavy Service on first request. */
|
|
2
|
-
export declare function generateExecutionEntry(serviceSpecifier: string,
|
|
11
|
+
export declare function generateExecutionEntry(serviceSpecifier: string, options?: ExecutionEntryOptions): string;
|
|
@@ -1,13 +1,46 @@
|
|
|
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, options = {}) {
|
|
3
|
+
const workerSpecifier = options.workerSpecifier ?? '@astrale-os/adapter-cloudflare/worker';
|
|
4
|
+
const frontSpecifier = options.frontSpecifier ?? '@astrale-os/adapter-cloudflare/worker/front';
|
|
5
|
+
const configuration = [
|
|
6
|
+
options.frontend === undefined
|
|
7
|
+
? ''
|
|
8
|
+
: ` frontend: ${JSON.stringify(options.frontend)} as const satisfies NonNullable<BuildMaterial['frontend']>,\n`,
|
|
9
|
+
options.frontendRevision === undefined
|
|
10
|
+
? ''
|
|
11
|
+
: ` frontendRevision: ${JSON.stringify(options.frontendRevision)},\n`,
|
|
12
|
+
options.frontendAddressed === undefined
|
|
13
|
+
? ''
|
|
14
|
+
: ` frontendAddressed: ${JSON.stringify(options.frontendAddressed)},\n`,
|
|
15
|
+
options.providerAssignedWorker === undefined
|
|
16
|
+
? ''
|
|
17
|
+
: ` providerAssignedWorker: ${JSON.stringify(options.providerAssignedWorker)},\n`,
|
|
18
|
+
].join('');
|
|
3
19
|
return `// AUTO-GENERATED by @astrale-os/adapter-cloudflare — do not edit.
|
|
20
|
+
import type { BuildMaterial } from '@astrale-os/sdk/deployment/build'
|
|
4
21
|
import type { CloudflareWorker } from ${JSON.stringify(workerSpecifier)}
|
|
22
|
+
import type { CloudflareAdapterBindings } from ${JSON.stringify(frontSpecifier)}
|
|
23
|
+
|
|
24
|
+
import { cloudflareFrontDoor } from ${JSON.stringify(frontSpecifier)}
|
|
25
|
+
|
|
26
|
+
type WorkerEnv = CloudflareEnv & CloudflareAdapterBindings
|
|
5
27
|
|
|
6
28
|
const SERVICE_SPECIFIER = ${JSON.stringify(serviceSpecifier)}
|
|
7
29
|
let servicePromise: Promise<CloudflareWorker<CloudflareEnv>> | undefined
|
|
8
30
|
|
|
31
|
+
// Admission and public frontend files answer from startup, so a View's document never waits for
|
|
32
|
+
// the execution Service to load. Each such file starts that load for the calls the View makes next.
|
|
33
|
+
const frontDoor = cloudflareFrontDoor<WorkerEnv>({
|
|
34
|
+
${configuration}})
|
|
35
|
+
|
|
9
36
|
export default {
|
|
10
|
-
async fetch(request: Request, environment:
|
|
37
|
+
async fetch(request: Request, environment: WorkerEnv, context: ExecutionContext) {
|
|
38
|
+
const admission = await frontDoor.admit(request, environment)
|
|
39
|
+
if (admission.kind === 'refused') return admission.response
|
|
40
|
+
if (admission.kind === 'frontend') {
|
|
41
|
+
context.waitUntil(prepare())
|
|
42
|
+
return admission.response
|
|
43
|
+
}
|
|
11
44
|
return (await service()).fetch(request, environment, context)
|
|
12
45
|
},
|
|
13
46
|
}
|
|
@@ -21,6 +54,13 @@ function service(): Promise<CloudflareWorker<CloudflareEnv>> {
|
|
|
21
54
|
}))
|
|
22
55
|
}
|
|
23
56
|
|
|
57
|
+
function prepare(): Promise<void> {
|
|
58
|
+
// A failure stays memoized by the Service and resurfaces on the request that needs it.
|
|
59
|
+
return service().then((worker) => worker.prepare?.()).then(ignore, ignore)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ignore(): void {}
|
|
63
|
+
|
|
24
64
|
function invalid(): never {
|
|
25
65
|
throw new TypeError('Cloudflare execution Service module is invalid.')
|
|
26
66
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generatedMaterial } from '@astrale-os/sdk/deployment/build';
|
|
1
2
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
4
|
import { SIGNING_IDENTITY_BINDING } from '../../identity/binding.js';
|
|
@@ -7,7 +8,7 @@ import { generateExecutionEntry, generateExecutionService } from '../codegen/exe
|
|
|
7
8
|
import { generateWranglerConfig } from '../codegen/wrangler.js';
|
|
8
9
|
import { workerInvocationTimeout, workerRequestLimit } from '../configuration.js';
|
|
9
10
|
import { qualifyWorkerBundle } from '../qualification.js';
|
|
10
|
-
import { workerModuleSpecifier } from './specifier.js';
|
|
11
|
+
import { frontModuleSpecifier, workerModuleSpecifier } from './specifier.js';
|
|
11
12
|
export const RELEASE_ENTRY = 'bundle/index.mjs';
|
|
12
13
|
export const RELEASE_SERVICE = 'bundle/service.mjs';
|
|
13
14
|
export async function prepareExecution(input) {
|
|
@@ -77,7 +78,19 @@ export async function prepareExecution(input) {
|
|
|
77
78
|
await mkdir(dirname(join(input.directory, RELEASE_SERVICE)), { recursive: true });
|
|
78
79
|
await writeFile(join(input.directory, RELEASE_SERVICE), service);
|
|
79
80
|
const sourcePath = join(input.directory, 'worker.gen.ts');
|
|
80
|
-
|
|
81
|
+
const frontend = generatedMaterial(input.context.build).material.frontend;
|
|
82
|
+
await writeFile(sourcePath, generateExecutionEntry('./service.mjs', {
|
|
83
|
+
workerSpecifier,
|
|
84
|
+
frontSpecifier: frontModuleSpecifier(),
|
|
85
|
+
...(frontend === undefined ? {} : { frontend }),
|
|
86
|
+
...(input.frontendRevision === undefined ? {} : { frontendRevision: input.frontendRevision }),
|
|
87
|
+
...(input.frontendAddressed === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: { frontendAddressed: input.frontendAddressed }),
|
|
90
|
+
...(input.configuredIssuer === undefined && input.direct
|
|
91
|
+
? { providerAssignedWorker: input.name }
|
|
92
|
+
: {}),
|
|
93
|
+
}));
|
|
81
94
|
const configPath = join(input.directory, 'wrangler.prepare.jsonc');
|
|
82
95
|
await writeFile(configPath, generateWranglerConfig({
|
|
83
96
|
workerName: input.name,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
const workerModule = import.meta.resolve('@astrale-os/adapter-cloudflare/worker');
|
|
3
|
+
const frontModule = import.meta.resolve('@astrale-os/adapter-cloudflare/worker/front');
|
|
3
4
|
export function workerModuleSpecifier() {
|
|
4
5
|
return fileURLToPath(workerModule);
|
|
5
6
|
}
|
|
7
|
+
/** The isolate-startup module, which must stay free of the execution Service's dependencies. */
|
|
8
|
+
export function frontModuleSpecifier() {
|
|
9
|
+
return fileURLToPath(frontModule);
|
|
10
|
+
}
|
package/dist/worker/entry.d.ts
CHANGED
|
@@ -2,33 +2,14 @@ import type { JsonWebKey } from '@astrale-os/sdk/auth';
|
|
|
2
2
|
import type { BuildMaterial, GeneratedBuildMaterial } from '@astrale-os/sdk/deployment/build/material';
|
|
3
3
|
import type { ExchangeOptions, ExecutionContext } from '@astrale-os/sdk/execution';
|
|
4
4
|
import type { Runtime } from '@astrale-os/sdk/runtime';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export interface CloudflareAdapterBindings {
|
|
9
|
-
readonly ASTRALE_SIGNING_IDENTITY?: string;
|
|
10
|
-
readonly ASTRALE_VERSION_METADATA?: {
|
|
11
|
-
readonly id: string;
|
|
12
|
-
};
|
|
13
|
-
readonly WORKER_URL?: string;
|
|
14
|
-
readonly IDENTITY_ISS?: string;
|
|
15
|
-
readonly ASSETS?: Fetcher;
|
|
16
|
-
readonly SELF?: Fetcher;
|
|
17
|
-
}
|
|
18
|
-
export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings extends CloudflareAdapterBindings, Environment, Material = BuildMaterial> {
|
|
19
|
-
/** Generated frontend evidence available without restoring the execution Build. */
|
|
20
|
-
readonly frontend?: BuildMaterial['frontend'];
|
|
21
|
-
/** Generated browser-file identity used only for deployment propagation checks. */
|
|
22
|
-
readonly frontendRevision?: string;
|
|
23
|
-
/** Served frontend paths the bundler content-addressed; absent means none may be retained. */
|
|
24
|
-
readonly frontendAddressed?: readonly string[];
|
|
5
|
+
import type { CloudflareAdapterBindings, CloudflareFrontDoorConfig, Fetcher } from './front.js';
|
|
6
|
+
export type { CloudflareAdapterBindings, Fetcher } from './front.js';
|
|
7
|
+
export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings extends CloudflareAdapterBindings, Environment, Material = BuildMaterial> extends CloudflareFrontDoorConfig<Bindings> {
|
|
25
8
|
/** Load the exact Runtime and Build once, after the isolate has accepted a request. */
|
|
26
9
|
readonly load: () => CloudflareWorkerSource<RuntimeValue, Material> | Promise<CloudflareWorkerSource<RuntimeValue, Material>>;
|
|
27
10
|
readonly privateKey: JsonWebKey | ((bindings: Bindings) => JsonWebKey);
|
|
28
11
|
readonly resolveEnvironment?: (bindings: Bindings) => Environment;
|
|
29
|
-
readonly resolveUrl?: (bindings: Bindings, requestOrigin: string) => string;
|
|
30
12
|
readonly identityIssuer?: string;
|
|
31
|
-
readonly providerAssignedWorker?: string;
|
|
32
13
|
readonly selfBinding?: (bindings: Bindings) => Fetcher | null | undefined;
|
|
33
14
|
readonly routeSubrequest?: (url: URL, bindings: Bindings) => Fetcher | null | undefined;
|
|
34
15
|
readonly invocationTimeoutMs?: number;
|
|
@@ -41,6 +22,8 @@ export interface CloudflareWorkerSource<RuntimeValue extends Runtime, Material =
|
|
|
41
22
|
}
|
|
42
23
|
export interface CloudflareWorker<Bindings> {
|
|
43
24
|
fetch(request: Request, environment: Bindings, executionContext?: ExecutionContext): Response | Promise<Response>;
|
|
25
|
+
/** Realize execution ahead of the request that needs it; failures resurface on that request. */
|
|
26
|
+
prepare?(): Promise<void>;
|
|
44
27
|
}
|
|
45
28
|
/** Compose one Service implementation behind its provider-owned stable address. */
|
|
46
29
|
export declare function cloudflareWorkerEntry<Bindings extends CloudflareAdapterBindings, Environment = Bindings>(): <RuntimeValue extends Runtime>(config: CloudflareWorkerConfig<RuntimeValue, Bindings, Environment, BuildMaterial>) => CloudflareWorker<Bindings>;
|
package/dist/worker/entry.js
CHANGED
|
@@ -3,6 +3,7 @@ import { restoreBuild, restoreGeneratedBuild } from '@astrale-os/sdk/deployment/
|
|
|
3
3
|
import { addressing, assemble } 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
|
+
import { cloudflareFrontDoor, withProviderVersion } from './front.js';
|
|
6
7
|
import { frontendAsset } from './frontend.js';
|
|
7
8
|
/** Compose one Service implementation behind its provider-owned stable address. */
|
|
8
9
|
export function cloudflareWorkerEntry() {
|
|
@@ -14,7 +15,7 @@ export function generatedCloudflareWorkerEntry() {
|
|
|
14
15
|
}
|
|
15
16
|
function createCloudflareWorkerEntry(restore) {
|
|
16
17
|
return function (config) {
|
|
17
|
-
|
|
18
|
+
const frontDoor = cloudflareFrontDoor(config);
|
|
18
19
|
let realized;
|
|
19
20
|
const realize = () => {
|
|
20
21
|
if (realized !== undefined)
|
|
@@ -26,7 +27,7 @@ function createCloudflareWorkerEntry(restore) {
|
|
|
26
27
|
const application = serve({
|
|
27
28
|
runtime: source.runtime,
|
|
28
29
|
load(environment) {
|
|
29
|
-
if (
|
|
30
|
+
if (frontDoor.current() !== environment.publicUrl) {
|
|
30
31
|
throw new TypeError('Cloudflare Worker request origin differs from its current Release.');
|
|
31
32
|
}
|
|
32
33
|
return assemble(build, addressing(config.identityIssuer ?? environment.publicUrl, {
|
|
@@ -69,28 +70,10 @@ function createCloudflareWorkerEntry(restore) {
|
|
|
69
70
|
};
|
|
70
71
|
return Object.freeze({
|
|
71
72
|
async fetch(request, bindings, context) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
requestOrigin(url, request));
|
|
77
|
-
if (config.providerAssignedWorker !== undefined &&
|
|
78
|
-
bindings.WORKER_URL === undefined &&
|
|
79
|
-
bindings.IDENTITY_ISS === undefined &&
|
|
80
|
-
!matchesWorkersDevClaim(publicUrl, config.providerAssignedWorker)) {
|
|
81
|
-
return new Response('Request origin differs from provider-assigned Addressing.', {
|
|
82
|
-
status: 421,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (currentPublicUrl !== undefined && currentPublicUrl !== publicUrl) {
|
|
86
|
-
return new Response('Request origin differs from the current Release.', { status: 421 });
|
|
87
|
-
}
|
|
88
|
-
currentPublicUrl = publicUrl;
|
|
89
|
-
if (config.frontend !== undefined) {
|
|
90
|
-
const frontend = await frontendAsset(config.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
|
|
91
|
-
if (frontend !== undefined)
|
|
92
|
-
return withProviderVersion(frontend, bindings);
|
|
93
|
-
}
|
|
73
|
+
const admission = await frontDoor.admit(request, bindings);
|
|
74
|
+
if (admission.kind !== 'execution')
|
|
75
|
+
return admission.response;
|
|
76
|
+
const { url, publicUrl } = admission;
|
|
94
77
|
const { application, build } = await realize();
|
|
95
78
|
if (config.frontend === undefined) {
|
|
96
79
|
const frontend = await frontendAsset(build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
|
|
@@ -100,16 +83,12 @@ function createCloudflareWorkerEntry(restore) {
|
|
|
100
83
|
const response = await application.fetch(request, Object.freeze({ bindings, publicUrl }), context);
|
|
101
84
|
return withProviderVersion(response, bindings);
|
|
102
85
|
},
|
|
86
|
+
async prepare() {
|
|
87
|
+
await realize();
|
|
88
|
+
},
|
|
103
89
|
});
|
|
104
90
|
};
|
|
105
91
|
}
|
|
106
|
-
function withProviderVersion(response, bindings) {
|
|
107
|
-
if (bindings.ASTRALE_VERSION_METADATA === undefined)
|
|
108
|
-
return response;
|
|
109
|
-
const versioned = new Response(response.body, response);
|
|
110
|
-
versioned.headers.set('X-Astrale-Worker-Version', bindings.ASTRALE_VERSION_METADATA.id);
|
|
111
|
-
return versioned;
|
|
112
|
-
}
|
|
113
92
|
function routedFetch(application, environment, config) {
|
|
114
93
|
return async (input, init) => {
|
|
115
94
|
const { request, rejectRedirect } = prepareCloudflareRequest(input, init);
|
|
@@ -124,36 +103,3 @@ function routedFetch(application, environment, config) {
|
|
|
124
103
|
return dispatchCloudflareRequest(request, rejectRedirect, (selected) => route === null || route === undefined ? fetch(selected) : route.fetch(selected));
|
|
125
104
|
};
|
|
126
105
|
}
|
|
127
|
-
function requestOrigin(url, request) {
|
|
128
|
-
if (url.protocol !== 'http:')
|
|
129
|
-
return url.origin;
|
|
130
|
-
const forwarded = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim().toLowerCase();
|
|
131
|
-
return forwarded === 'https' ? `https://${url.host}` : url.origin;
|
|
132
|
-
}
|
|
133
|
-
function canonicalServingUrl(input) {
|
|
134
|
-
let url;
|
|
135
|
-
try {
|
|
136
|
-
url = new URL(input);
|
|
137
|
-
}
|
|
138
|
-
catch (cause) {
|
|
139
|
-
throw new TypeError('Cloudflare Worker serving URL is invalid.', { cause });
|
|
140
|
-
}
|
|
141
|
-
if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
|
|
142
|
-
url.username !== '' ||
|
|
143
|
-
url.password !== '' ||
|
|
144
|
-
url.pathname !== '/' ||
|
|
145
|
-
url.search !== '' ||
|
|
146
|
-
url.hash !== '') {
|
|
147
|
-
throw new TypeError('Cloudflare Worker serving URL must be one HTTP(S) origin.');
|
|
148
|
-
}
|
|
149
|
-
return url.origin;
|
|
150
|
-
}
|
|
151
|
-
function matchesWorkersDevClaim(issuer, expectedWorker) {
|
|
152
|
-
const url = new URL(issuer);
|
|
153
|
-
const labels = url.hostname.split('.');
|
|
154
|
-
return (url.protocol === 'https:' &&
|
|
155
|
-
labels.length >= 4 &&
|
|
156
|
-
labels[0] === expectedWorker &&
|
|
157
|
-
labels.at(-2) === 'workers' &&
|
|
158
|
-
labels.at(-1) === 'dev');
|
|
159
|
-
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BuildMaterial } from '@astrale-os/sdk/deployment/build/material';
|
|
2
|
+
export interface Fetcher {
|
|
3
|
+
fetch(request: Request): Response | Promise<Response>;
|
|
4
|
+
}
|
|
5
|
+
export interface CloudflareAdapterBindings {
|
|
6
|
+
readonly ASTRALE_SIGNING_IDENTITY?: string;
|
|
7
|
+
readonly ASTRALE_VERSION_METADATA?: {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
};
|
|
10
|
+
readonly WORKER_URL?: string;
|
|
11
|
+
readonly IDENTITY_ISS?: string;
|
|
12
|
+
readonly ASSETS?: Fetcher;
|
|
13
|
+
readonly SELF?: Fetcher;
|
|
14
|
+
}
|
|
15
|
+
export interface CloudflareFrontDoorConfig<Bindings extends CloudflareAdapterBindings> {
|
|
16
|
+
/** Generated frontend evidence available without restoring the execution Build. */
|
|
17
|
+
readonly frontend?: BuildMaterial['frontend'];
|
|
18
|
+
/** Generated browser-file identity used only for deployment propagation checks. */
|
|
19
|
+
readonly frontendRevision?: string;
|
|
20
|
+
/** Served frontend paths the bundler content-addressed; absent means none may be retained. */
|
|
21
|
+
readonly frontendAddressed?: readonly string[];
|
|
22
|
+
readonly resolveUrl?: (bindings: Bindings, requestOrigin: string) => string;
|
|
23
|
+
readonly providerAssignedWorker?: string;
|
|
24
|
+
}
|
|
25
|
+
export type CloudflareFrontDoorAdmission =
|
|
26
|
+
/** The request is not for this isolate's serving origin. */
|
|
27
|
+
{
|
|
28
|
+
readonly kind: 'refused';
|
|
29
|
+
readonly response: Response;
|
|
30
|
+
}
|
|
31
|
+
/** A public frontend file answered the request without the execution Service. */
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: 'frontend';
|
|
34
|
+
readonly response: Response;
|
|
35
|
+
}
|
|
36
|
+
/** The request needs the execution Service, under this isolate's one serving origin. */
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: 'execution';
|
|
39
|
+
readonly url: URL;
|
|
40
|
+
readonly publicUrl: string;
|
|
41
|
+
};
|
|
42
|
+
export interface CloudflareFrontDoor<Bindings extends CloudflareAdapterBindings> {
|
|
43
|
+
admit(request: Request, bindings: Bindings): Promise<CloudflareFrontDoorAdmission>;
|
|
44
|
+
/** The serving origin this isolate pinned on its first admitted request. */
|
|
45
|
+
current(): string | undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Admit requests to one serving origin and answer public frontend files, per isolate. */
|
|
48
|
+
export declare function cloudflareFrontDoor<Bindings extends CloudflareAdapterBindings>(config: CloudflareFrontDoorConfig<Bindings>): CloudflareFrontDoor<Bindings>;
|
|
49
|
+
export declare function withProviderVersion(response: Response, bindings: CloudflareAdapterBindings): Response;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { frontendAsset } from './frontend.js';
|
|
2
|
+
/** Admit requests to one serving origin and answer public frontend files, per isolate. */
|
|
3
|
+
export function cloudflareFrontDoor(config) {
|
|
4
|
+
let currentPublicUrl;
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
async admit(request, bindings) {
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
const publicUrl = canonicalServingUrl(bindings.WORKER_URL ??
|
|
9
|
+
bindings.IDENTITY_ISS ??
|
|
10
|
+
config.resolveUrl?.(bindings, requestOrigin(url, request)) ??
|
|
11
|
+
requestOrigin(url, request));
|
|
12
|
+
if (config.providerAssignedWorker !== undefined &&
|
|
13
|
+
bindings.WORKER_URL === undefined &&
|
|
14
|
+
bindings.IDENTITY_ISS === undefined &&
|
|
15
|
+
!matchesWorkersDevClaim(publicUrl, config.providerAssignedWorker)) {
|
|
16
|
+
return refused('Request origin differs from provider-assigned Addressing.');
|
|
17
|
+
}
|
|
18
|
+
if (currentPublicUrl !== undefined && currentPublicUrl !== publicUrl) {
|
|
19
|
+
return refused('Request origin differs from the current Release.');
|
|
20
|
+
}
|
|
21
|
+
currentPublicUrl = publicUrl;
|
|
22
|
+
if (config.frontend !== undefined) {
|
|
23
|
+
const frontend = await frontendAsset(config.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
|
|
24
|
+
if (frontend !== undefined) {
|
|
25
|
+
return { kind: 'frontend', response: withProviderVersion(frontend, bindings) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { kind: 'execution', url, publicUrl };
|
|
29
|
+
},
|
|
30
|
+
current: () => currentPublicUrl,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function withProviderVersion(response, bindings) {
|
|
34
|
+
if (bindings.ASTRALE_VERSION_METADATA === undefined)
|
|
35
|
+
return response;
|
|
36
|
+
const versioned = new Response(response.body, response);
|
|
37
|
+
versioned.headers.set('X-Astrale-Worker-Version', bindings.ASTRALE_VERSION_METADATA.id);
|
|
38
|
+
return versioned;
|
|
39
|
+
}
|
|
40
|
+
function refused(message) {
|
|
41
|
+
return { kind: 'refused', response: new Response(message, { status: 421 }) };
|
|
42
|
+
}
|
|
43
|
+
function requestOrigin(url, request) {
|
|
44
|
+
if (url.protocol !== 'http:')
|
|
45
|
+
return url.origin;
|
|
46
|
+
const forwarded = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim().toLowerCase();
|
|
47
|
+
return forwarded === 'https' ? `https://${url.host}` : url.origin;
|
|
48
|
+
}
|
|
49
|
+
function canonicalServingUrl(input) {
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(input);
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
throw new TypeError('Cloudflare Worker serving URL is invalid.', { cause });
|
|
56
|
+
}
|
|
57
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
|
|
58
|
+
url.username !== '' ||
|
|
59
|
+
url.password !== '' ||
|
|
60
|
+
url.pathname !== '/' ||
|
|
61
|
+
url.search !== '' ||
|
|
62
|
+
url.hash !== '') {
|
|
63
|
+
throw new TypeError('Cloudflare Worker serving URL must be one HTTP(S) origin.');
|
|
64
|
+
}
|
|
65
|
+
return url.origin;
|
|
66
|
+
}
|
|
67
|
+
function matchesWorkersDevClaim(issuer, expectedWorker) {
|
|
68
|
+
const url = new URL(issuer);
|
|
69
|
+
const labels = url.hostname.split('.');
|
|
70
|
+
return (url.protocol === 'https:' &&
|
|
71
|
+
labels.length >= 4 &&
|
|
72
|
+
labels[0] === expectedWorker &&
|
|
73
|
+
labels.at(-2) === 'workers' &&
|
|
74
|
+
labels.at(-1) === 'dev');
|
|
75
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { BuildMaterial } from '@astrale-os/sdk/deployment/build/material';
|
|
2
|
-
import type { Fetcher } from './
|
|
2
|
+
import type { Fetcher } from './front.js';
|
|
3
3
|
/** Serve public frontend files without realizing the Application's execution Runtime. */
|
|
4
4
|
export declare function frontendAsset(frontend: BuildMaterial['frontend'], assets: Fetcher | undefined, url: URL, request: Request, revision?: string, addressed?: readonly string[]): Promise<Response | undefined>;
|
package/dist/worker/frontend.js
CHANGED
|
@@ -41,7 +41,11 @@ export async function frontendAsset(frontend, assets, url, request, revision, ad
|
|
|
41
41
|
function retainable(url, addressed) {
|
|
42
42
|
if (addressed === undefined || addressed.length === 0)
|
|
43
43
|
return false;
|
|
44
|
-
|
|
44
|
+
const path = decodeURIComponent(url.pathname).replace(/^\//u, '');
|
|
45
|
+
// A document's 304 carries no media type to recognise it by, so its name has to.
|
|
46
|
+
if (/\.html?$/iu.test(path))
|
|
47
|
+
return false;
|
|
48
|
+
return addressed.includes(path);
|
|
45
49
|
}
|
|
46
50
|
function publicAsset(response, revision, addressed) {
|
|
47
51
|
// Plain Views have an opaque sandbox origin. Their modules, CSS and fonts still need
|
|
@@ -55,11 +59,15 @@ function publicAsset(response, revision, addressed) {
|
|
|
55
59
|
// would tell that browser to discard what it was just told to keep, so the next visit downloads
|
|
56
60
|
// the file again and every revalidation costs a full transfer: worse than never caching at all.
|
|
57
61
|
const validated = response.status === 304;
|
|
58
|
-
//
|
|
59
|
-
if (
|
|
60
|
-
response.headers.get('Content-Type')?.split(';')[0] === 'text/html') {
|
|
62
|
+
// Do not retain transient asset misses.
|
|
63
|
+
if (!response.ok && !validated) {
|
|
61
64
|
readable.headers.set('Cache-Control', 'no-store');
|
|
62
65
|
}
|
|
66
|
+
else if (response.headers.get('Content-Type')?.split(';')[0] === 'text/html') {
|
|
67
|
+
// Documents select hashed assets that a later Release removes, so a document is never used
|
|
68
|
+
// without asking first. Keeping it still lets that question be answered by a bodiless 304.
|
|
69
|
+
readable.headers.set('Cache-Control', 'no-cache');
|
|
70
|
+
}
|
|
63
71
|
else if (addressed) {
|
|
64
72
|
// The bytes behind this name can never change, so a revalidation could only ever confirm what
|
|
65
73
|
// the name already states. `immutable` is deliberately absent: it would also suppress the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/adapter-cloudflare",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.159",
|
|
4
4
|
"description": "Deploy an Astrale application through Cloudflare Workers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"types": "./dist/worker/index.d.ts",
|
|
44
44
|
"import": "./dist/worker/index.js"
|
|
45
45
|
},
|
|
46
|
+
"./worker/front": {
|
|
47
|
+
"types": "./dist/worker/front.d.ts",
|
|
48
|
+
"import": "./dist/worker/front.js"
|
|
49
|
+
},
|
|
46
50
|
"./release": {
|
|
47
51
|
"types": "./dist/release/index.d.ts",
|
|
48
52
|
"import": "./dist/release/index.js"
|
|
@@ -53,7 +57,7 @@
|
|
|
53
57
|
"registry": "https://registry.npmjs.org/"
|
|
54
58
|
},
|
|
55
59
|
"dependencies": {
|
|
56
|
-
"@astrale-os/sdk": "0.6.0-beta.
|
|
60
|
+
"@astrale-os/sdk": "0.6.0-beta.4",
|
|
57
61
|
"@noble/hashes": "2.3.0",
|
|
58
62
|
"hono": "^4.6.20",
|
|
59
63
|
"jose": "^6.1.3",
|