ngx-cloudflare 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erkam Yaman (erkamyaman)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,176 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">ngx-cloudflare</h1>
2
+ <p align="center">
3
+ <img src="https://img.shields.io/maintenance/yes/2026?style=for-the-badge" alt="Maintained: yes" />
4
+ <img src="https://img.shields.io/badge/angular-19%20to%2022-dd0031?style=for-the-badge&logo=angular&logoColor=white" alt="Angular 19 to 22" />
5
+ <img src="https://img.shields.io/badge/cloudflare-workers-f38020?style=for-the-badge&logo=cloudflare&logoColor=white" alt="Cloudflare Workers" />
6
+ <a href="https://github.com/erkamyaman/ngx-cloudflare/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/erkamyaman/ngx-cloudflare/ci.yml?branch=main&style=for-the-badge&label=CI" alt="CI status" /></a>
7
+ <a href="https://github.com/erkamyaman/ngx-cloudflare/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=for-the-badge" alt="License: MIT" /></a>
8
+ <a href="https://github.com/sponsors/erkamyaman"><img src="https://img.shields.io/badge/sponsor-%E2%9D%A4-db61a2?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Sponsor" /></a>
9
+ </p>
10
+ <p align="center">
11
+ Workers bindings, a working hostname allow-list and in-process API calls for Angular SSR apps on Cloudflare Workers, in <code>ng serve</code> and in production.
12
+ </p>
2
13
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
14
+ <table align="center">
15
+ <tr>
16
+ <td align="center"><a href="https://github.com/erkamyaman/ngx-cloudflare/blob/main/docs/api.md"><strong>API</strong></a></td>
17
+ <td align="center"><a href="https://github.com/erkamyaman/ngx-cloudflare/blob/main/docs/recipes.md">Recipes</a></td>
18
+ <td align="center"><a href="https://github.com/erkamyaman/ngx-cloudflare/blob/main/docs/troubleshooting.md">Troubleshooting</a></td>
19
+ <td align="center"><a href="#the-demo">Demo</a></td>
20
+ </tr>
21
+ </table>
22
+
23
+ > [!NOTE]
24
+ > This is an early 0.x release. Function names may still change before 1.0.0.
25
+
26
+ ## Why
27
+
28
+ Cloudflare's Angular starter gets an app onto Workers, but four things break once it is real:
29
+
30
+ | Problem | Without ngx-cloudflare | With it |
31
+ | --- | --- | --- |
32
+ | Hostnames | The generated `server.ts` allows only `localhost`, so Angular answers `400 Header "host" ... is not allowed` on your `*.workers.dev` address and custom domains. | `localhost`, `127.0.0.1`, `*.workers.dev` and your wrangler routes are allowed. Anything else gets a 400 that says where to add it. |
33
+ | Bindings | `env`, `ctx` and `request.cf` never reach Angular code, and `ng serve` has no bindings at all. | `injectEnv()` and friends, on Workers and in `ng serve`, sharing local state with `wrangler dev`. |
34
+ | Own-API calls | During rendering, `HttpClient.get('/api/...')` leaves the Worker and fetches its own hostname. | Answered inside the Worker, and Angular's transfer cache still hands the result to the browser. |
35
+ | Log noise | Angular logs a warning for every `x-forwarded-*` header Cloudflare adds, on every request. | Those headers are removed before Angular sees them. |
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ ng add ngx-cloudflare
41
+ ```
42
+
43
+ Run it on an app with server-side rendering (`ng new --ssr`, or after `ng add @angular/ssr`). It also works on projects made by `npm create cloudflare` and `wrangler setup`. It:
44
+
45
+ - points the build at Workers (`outputMode: server`, the neutral SSR platform, `wrangler` kept out of the bundle),
46
+ - replaces a stock `src/server.ts` (Angular's Express server or Cloudflare's untouched template, keeping any hosts you added to it) and never touches one you customised,
47
+ - copies the hostnames in your wrangler routes into `ALLOWED_HOSTS` in `wrangler.jsonc`, again each time you run it,
48
+ - registers `provideCloudflare()` in the server config,
49
+ - creates `wrangler.jsonc`, or repairs one that deploys the app as a static site,
50
+ - switches the stock prerender-everything route to per-request rendering,
51
+ - adds `preview`, `deploy` and `cf-typegen` scripts and removes the broken `serve:ssr` one.
52
+
53
+ | Angular | Platform key it sets | Notes |
54
+ | --- | --- | --- |
55
+ | 22 | `ssr.platform` | |
56
+ | 21 | `ssr.experimentalPlatform` | |
57
+ | 20 | `ssr.experimentalPlatform` | |
58
+ | 19 | `ssr.experimentalPlatform` | Apps created without server routing are converted: `ng add` adds `app.routes.server.ts` and registers it. |
59
+ | 18 and older | | Not supported. `AngularAppEngine` and `REQUEST_CONTEXT`, which this builds on, arrived in 19. |
60
+
61
+ None of them needs the `nodejs_compat` flag.
62
+
63
+ ## Usage
64
+
65
+ `src/server.ts` after `ng add`:
66
+
67
+ ```ts
68
+ import { createCloudflareRequestHandler } from 'ngx-cloudflare/server';
69
+
70
+ export const reqHandler = createCloudflareRequestHandler<Env>({
71
+ allowedHosts: ['shop.example.com'],
72
+ api: (request, env, ctx) => {
73
+ if (new URL(request.url).pathname === '/api/hello') {
74
+ return Response.json({ hello: env.GREETING });
75
+ }
76
+ return null;
77
+ },
78
+ });
79
+
80
+ export default { fetch: reqHandler } satisfies ExportedHandler<Env>;
81
+ ```
82
+
83
+ Custom domains can also live next to your routes in `wrangler.jsonc`, so adding one needs no code change:
84
+
85
+ ```jsonc
86
+ "routes": [{ "pattern": "shop.example.com", "custom_domain": true }],
87
+ "vars": { "ALLOWED_HOSTS": "shop.example.com" }
88
+ ```
89
+
90
+ `api` takes a function or anything with a `fetch` method, such as a [Hono](https://hono.dev) app. It receives requests under `/api/`. Return `null` to let Angular handle the path.
91
+
92
+ In components, services, resolvers and guards:
93
+
94
+ ```ts
95
+ import { injectEnv, injectExecutionContext, injectRequestCf } from 'ngx-cloudflare';
96
+
97
+ export class Visits {
98
+ private readonly env = injectEnv<Env>(); // null in the browser
99
+
100
+ constructor() {
101
+ const ctx = injectExecutionContext();
102
+ if (this.env && ctx) {
103
+ ctx.waitUntil(this.env.VISITS.put('last', new Date().toISOString()));
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Each function returns `null` outside a server request: in the browser, while prerendering and at build time. Pass `{ required: true }` to throw a clear error instead.
110
+
111
+ Run `npm run cf-typegen` after editing `wrangler.jsonc` to regenerate the `Env` type.
112
+
113
+ ## Develop and deploy
114
+
115
+ ```bash
116
+ ng serve # bindings from wrangler's local platform proxy
117
+ npm run preview # production build in the local Workers runtime
118
+ npm run deploy # production build, then wrangler deploy
119
+ ```
120
+
121
+ `ng serve` and `wrangler dev` share local state in `.wrangler/`, so data you add in one shows up in the other.
122
+
123
+ ## The demo
124
+
125
+ This repository is an Angular workspace. The root app is a guestbook that runs on the library: entries in D1, a visit counter in KV, and a form posting to `/api/entries`. It doubles as the end-to-end test target.
126
+
127
+ ```bash
128
+ npm ci
129
+ npm run build:lib
130
+ npm run db:migrate # create the local D1 table
131
+ npm start # or: npm run preview
132
+ ```
133
+
134
+ ## Tests
135
+
136
+ ```bash
137
+ npm run test:lib # library: server core, inject functions, HTTP backend, ng add
138
+ scripts/test-lib-angular.sh 19 # the library's Angular tests against Angular 19 (or 20, 21, 22)
139
+ npx ng test ngx-cloudflare-demo # demo
140
+ scripts/e2e-ng-add.sh 22 <package.tgz> [kind] # create an app, ng add, then check wrangler dev and ng serve
141
+ ```
142
+
143
+ `kind` is `ng-new` (default), `cloudflare` for a project made by `npm create cloudflare`, or `wrangler-setup` for one configured by `wrangler setup`.
144
+
145
+ CI runs the library tests on Angular 19, 20, 21 and 22, the end-to-end script on a fresh app for each of those versions, and on the Cloudflare starter and a `wrangler setup` project.
146
+
147
+ ## Beyond the basics
148
+
149
+ These ship with the package too, for cases the core flow does not cover:
150
+
151
+ - `ngx-cloudflare/testing`: fake bindings for TestBed.
152
+ - `isInProcessRequest(request)`: lets an `api` handler tell a render-time call from a browser call.
153
+ - `dispatch` falls back to the `ASSETS` binding, so a page can read its own static JSON files in-process.
154
+ - Options to turn parts off: `apiPrefix: false`, `stripProxyHeaders: false`, `devPlatform: false`, `provideCloudflare({ inProcessRequests: false })`.
155
+ - A [Durable Objects recipe](https://github.com/erkamyaman/ngx-cloudflare/blob/main/docs/recipes.md#durable-objects).
156
+
157
+ ## Limits
158
+
159
+ - Durable Objects and anything else that imports `cloudflare:workers` cannot live in `src/server.ts`, because Angular runs that file in Node. See the [Durable Objects recipe](https://github.com/erkamyaman/ngx-cloudflare/blob/main/docs/recipes.md#durable-objects).
160
+ - Routes rendered at build time (`RenderMode.Prerender`) have no bindings.
161
+ - Cloudflare Pages and Analog are not supported. Analog reaches Workers through Nitro's own preset.
162
+
163
+ ## Roadmap
164
+
165
+ | Next | Why |
166
+ | --- | --- |
167
+ | Durable Objects in `ng serve` | The recipe works in `wrangler dev` and on Workers, but the platform proxy cannot run an internal Durable Object. |
168
+ | Workers beside an existing Node server | A separate build configuration, so adopting this does not replace a working deployment. |
169
+ | Bindings during prerendering | Build-time rendering has no request to carry them. |
170
+ | Typed helpers for queues and cron | `scheduled` and `queue` exports already work next to `fetch`; helpers would add types. |
171
+
172
+ ## Trademarks and licence
173
+
174
+ MIT licensed, see [LICENSE](https://github.com/erkamyaman/ngx-cloudflare/blob/main/LICENSE).
175
+
176
+ Cloudflare and Cloudflare Workers are trademarks of Cloudflare, Inc. Angular is a trademark of Google LLC. This project is not affiliated with, endorsed by, or sponsored by either.
@@ -0,0 +1,174 @@
1
+ import { AngularAppEngine, createRequestHandler } from "@angular/ssr";
2
+ const CLOUDFLARE_CONTEXT_MARKER = Symbol.for("ngx-cloudflare.context");
3
+ const DEFAULT_ALLOWED_HOSTS = [
4
+ "localhost",
5
+ "127.0.0.1",
6
+ "*.workers.dev"
7
+ ];
8
+ const DEFAULT_API_PREFIX = "/api/";
9
+ const MAX_LOGGED_REFUSALS = 100;
10
+ const inProcessRequests = /* @__PURE__ */ new WeakSet();
11
+ function isInProcessRequest(request) {
12
+ return inProcessRequests.has(request);
13
+ }
14
+ function isCloudflareContext(value) {
15
+ return typeof value === "object" && value !== null && value[CLOUDFLARE_CONTEXT_MARKER] === true;
16
+ }
17
+ const ALLOWED_HOSTS_VAR = "ALLOWED_HOSTS";
18
+ function hostsFromEnv(env) {
19
+ const value = env?.[ALLOWED_HOSTS_VAR];
20
+ if (typeof value !== "string") return [];
21
+ return value.split(/[\s,]+/).map((host) => host.trim()).filter(Boolean);
22
+ }
23
+ function resolveAllowedHosts(extra = []) {
24
+ const hosts = [];
25
+ for (const host of [...DEFAULT_ALLOWED_HOSTS, ...extra]) {
26
+ const normalized = host.trim().toLowerCase();
27
+ if (normalized && !hosts.includes(normalized)) hosts.push(normalized);
28
+ }
29
+ return hosts;
30
+ }
31
+ function isHostAllowed(hostname, allowedHosts) {
32
+ const host = hostname.toLowerCase().replace(/\.$/, "");
33
+ for (const pattern of allowedHosts) {
34
+ if (pattern === "*") return true;
35
+ if (pattern.startsWith("*.")) {
36
+ const suffix = pattern.slice(1);
37
+ if (host.endsWith(suffix) && host.length > suffix.length) return true;
38
+ } else if (host === pattern) return true;
39
+ }
40
+ return false;
41
+ }
42
+ function hostRefusal(hostname) {
43
+ return new Response(`Host "${hostname}" is not allowed by ngx-cloudflare. Add it to ALLOWED_HOSTS in the vars of wrangler.jsonc, or to allowedHosts in src/server.ts. localhost, 127.0.0.1 and *.workers.dev are allowed by default.`, {
44
+ status: 400,
45
+ headers: { "content-type": "text/plain; charset=utf-8" }
46
+ });
47
+ }
48
+ function isProxyHeader(name) {
49
+ const lower = name.toLowerCase();
50
+ return lower === "forwarded" || lower.startsWith("x-forwarded-");
51
+ }
52
+ function stripProxyHeaders(request) {
53
+ const names = [];
54
+ request.headers.forEach((_value, name) => {
55
+ if (isProxyHeader(name)) names.push(name);
56
+ });
57
+ if (names.length === 0) return request;
58
+ const headers = new Headers(request.headers);
59
+ for (const name of names) headers.delete(name);
60
+ return new Request(request, { headers });
61
+ }
62
+ function toApiHandler(api) {
63
+ if (!api) return;
64
+ if (typeof api === "function") return api;
65
+ return (request, env, ctx) => api.fetch(request, env, ctx);
66
+ }
67
+ function matchesPrefix(request, prefix) {
68
+ if (prefix === false) return true;
69
+ const { pathname } = new URL(request.url);
70
+ const bare = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
71
+ return pathname === bare || pathname.startsWith(bare + "/");
72
+ }
73
+ async function callInProcess(handler, request, env, ctx) {
74
+ inProcessRequests.add(request);
75
+ return await handler(request, env, ctx) ?? null;
76
+ }
77
+ const NOOP_CONTEXT = {
78
+ waitUntil() {},
79
+ passThroughOnException() {}
80
+ };
81
+ function createCloudflareFetch(render, options = {}, loadDevPlatform) {
82
+ const configuredHosts = options.allowedHosts ?? [];
83
+ const hostsByEnv = /* @__PURE__ */ new WeakMap();
84
+ const allowedHostsFor = (env) => {
85
+ if (typeof env !== "object" || env === null) return resolveAllowedHosts(configuredHosts);
86
+ let hosts = hostsByEnv.get(env);
87
+ if (!hosts) {
88
+ hosts = resolveAllowedHosts([...configuredHosts, ...hostsFromEnv(env)]);
89
+ hostsByEnv.set(env, hosts);
90
+ }
91
+ return hosts;
92
+ };
93
+ const api = toApiHandler(options.api);
94
+ const apiPrefix = options.apiPrefix ?? "/api/";
95
+ const strip = options.stripProxyHeaders ?? true;
96
+ const loggedRefusals = /* @__PURE__ */ new Set();
97
+ async function callApi(request, platform) {
98
+ if (!api || !matchesPrefix(request, apiPrefix)) return null;
99
+ return await api(request, platform?.env, platform?.ctx ?? NOOP_CONTEXT) ?? null;
100
+ }
101
+ async function serveAsset(request, platform) {
102
+ const assets = platform?.env?.ASSETS;
103
+ if (!assets || typeof assets.fetch !== "function" || request.method !== "GET" && request.method !== "HEAD") return null;
104
+ const response = await assets.fetch(request);
105
+ return response.status === 404 ? null : response;
106
+ }
107
+ function createContext(platform, source) {
108
+ return {
109
+ env: platform.env,
110
+ ctx: platform.ctx,
111
+ cf: platform.cf,
112
+ source,
113
+ dispatch: async (request) => {
114
+ inProcessRequests.add(request);
115
+ return await callApi(request, platform) ?? await serveAsset(request, platform);
116
+ },
117
+ [CLOUDFLARE_CONTEXT_MARKER]: true
118
+ };
119
+ }
120
+ return async (request, env, ctx) => {
121
+ let platform;
122
+ let source = "workers";
123
+ if (env !== void 0) platform = {
124
+ env,
125
+ ctx: ctx ?? NOOP_CONTEXT,
126
+ cf: request.cf
127
+ };
128
+ else if (loadDevPlatform) {
129
+ platform = await loadDevPlatform();
130
+ source = "platform-proxy";
131
+ }
132
+ const { hostname } = new URL(request.url);
133
+ const allowedHosts = allowedHostsFor(platform?.env);
134
+ if (!isHostAllowed(hostname, allowedHosts)) {
135
+ if (!loggedRefusals.has(hostname)) {
136
+ if (loggedRefusals.size >= MAX_LOGGED_REFUSALS) loggedRefusals.clear();
137
+ loggedRefusals.add(hostname);
138
+ console.error(`ngx-cloudflare: refused host "${hostname}". Allowed hosts: ${allowedHosts.join(", ")}`);
139
+ }
140
+ return hostRefusal(hostname);
141
+ }
142
+ const apiResponse = await callApi(request, platform);
143
+ if (apiResponse) return apiResponse;
144
+ const context = platform ? createContext(platform, source) : void 0;
145
+ return await render(strip ? stripProxyHeaders(request) : request, context) ?? new Response("Page not found.", { status: 404 });
146
+ };
147
+ }
148
+ function loadPlatformProxy(options) {
149
+ const cache = globalThis;
150
+ cache.__ngxCloudflarePlatformProxy ??= import("wrangler").then((wrangler) => wrangler.getPlatformProxy(options)).then((proxy) => ({
151
+ env: proxy.env,
152
+ ctx: proxy.ctx,
153
+ cf: proxy.cf
154
+ })).catch((error) => {
155
+ cache.__ngxCloudflarePlatformProxy = void 0;
156
+ console.error("ngx-cloudflare: could not start wrangler's platform proxy, so bindings are unavailable in ng serve. Install wrangler as a dev dependency and check wrangler.jsonc. The next request tries again.", error);
157
+ });
158
+ return cache.__ngxCloudflarePlatformProxy;
159
+ }
160
+ function createCloudflareRequestHandler(options = {}) {
161
+ let engine;
162
+ const devPlatform = options.devPlatform;
163
+ const handler = createCloudflareFetch((request, context) => {
164
+ engine ??= new AngularAppEngine({ allowedHosts: resolveAllowedHosts([...options.allowedHosts ?? [], ...hostsFromEnv(context?.env)]) });
165
+ return engine.handle(request, context);
166
+ }, options, () => {
167
+ if ((typeof ngDevMode === "undefined" || ngDevMode) && devPlatform !== false) return loadPlatformProxy(devPlatform ?? {});
168
+ });
169
+ createRequestHandler(handler);
170
+ return handler;
171
+ }
172
+ export { ALLOWED_HOSTS_VAR, CLOUDFLARE_CONTEXT_MARKER, DEFAULT_ALLOWED_HOSTS, DEFAULT_API_PREFIX, callInProcess, createCloudflareFetch, createCloudflareRequestHandler, hostRefusal, hostsFromEnv, isCloudflareContext, isHostAllowed, isInProcessRequest, isProxyHeader, resolveAllowedHosts, stripProxyHeaders, toApiHandler };
173
+
174
+ //# sourceMappingURL=ngx-cloudflare-server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-cloudflare-server.mjs","names":[],"sources":["../../../projects/ngx-cloudflare/server/src/cloudflare-fetch.ts","../../../projects/ngx-cloudflare/server/src/cloudflare-request-handler.ts"],"sourcesContent":["declare global {\n namespace Cloudflare {\n interface Env {}\n }\n}\n\nexport const CLOUDFLARE_CONTEXT_MARKER: unique symbol = Symbol.for(\n 'ngx-cloudflare.context',\n) as never;\n\nexport const DEFAULT_ALLOWED_HOSTS: readonly string[] = ['localhost', '127.0.0.1', '*.workers.dev'];\n\nexport const DEFAULT_API_PREFIX = '/api/';\n\nconst MAX_LOGGED_REFUSALS = 100;\n\nexport interface CloudflareExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n\nexport type CloudflareRequestCf = Record<string, unknown>;\n\nexport type CloudflareContextSource = 'workers' | 'platform-proxy';\n\nexport interface CloudflarePlatform<Env = Cloudflare.Env, Cf = CloudflareRequestCf> {\n readonly env: Env;\n readonly ctx: CloudflareExecutionContext;\n readonly cf: Cf | undefined;\n}\n\nexport interface CloudflareContext<\n Env = Cloudflare.Env,\n Cf = CloudflareRequestCf,\n> extends CloudflarePlatform<Env, Cf> {\n readonly source: CloudflareContextSource;\n readonly dispatch: (request: Request) => Promise<Response | null>;\n readonly [CLOUDFLARE_CONTEXT_MARKER]: true;\n}\n\nexport type ApiResult = Response | null | undefined;\n\nexport type ApiHandler<Env = Cloudflare.Env> = (\n request: Request,\n env: Env,\n ctx: CloudflareExecutionContext,\n) => ApiResult | Promise<ApiResult>;\n\nexport type ApiOption<Env = Cloudflare.Env> = ApiHandler<Env> | { fetch: ApiHandler<Env> };\n\nexport interface CloudflareHandlerOptions<Env = Cloudflare.Env> {\n allowedHosts?: readonly string[];\n api?: ApiOption<Env>;\n apiPrefix?: string | false;\n stripProxyHeaders?: boolean;\n}\n\nexport type RenderFunction = (\n request: Request,\n context: CloudflareContext<any, any> | undefined,\n) => Promise<Response | null>;\n\nexport type DevPlatformLoader = () => Promise<CloudflarePlatform<any, any> | undefined> | undefined;\n\nexport type CloudflareFetchHandler<Env = Cloudflare.Env> = (\n request: Request,\n env?: Env,\n ctx?: CloudflareExecutionContext,\n) => Promise<Response>;\n\nconst inProcessRequests = new WeakSet<Request>();\n\nexport function isInProcessRequest(request: Request): boolean {\n return inProcessRequests.has(request);\n}\n\nexport function isCloudflareContext(value: unknown): value is CloudflareContext<any, any> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[CLOUDFLARE_CONTEXT_MARKER] === true\n );\n}\n\nexport const ALLOWED_HOSTS_VAR = 'ALLOWED_HOSTS';\n\nexport function hostsFromEnv(env: unknown): string[] {\n const value = (env as Record<string, unknown> | undefined)?.[ALLOWED_HOSTS_VAR];\n if (typeof value !== 'string') {\n return [];\n }\n return value\n .split(/[\\s,]+/)\n .map((host) => host.trim())\n .filter(Boolean);\n}\n\nexport function resolveAllowedHosts(extra: readonly string[] = []): string[] {\n const hosts: string[] = [];\n for (const host of [...DEFAULT_ALLOWED_HOSTS, ...extra]) {\n const normalized = host.trim().toLowerCase();\n if (normalized && !hosts.includes(normalized)) {\n hosts.push(normalized);\n }\n }\n return hosts;\n}\n\nexport function isHostAllowed(hostname: string, allowedHosts: readonly string[]): boolean {\n const host = hostname.toLowerCase().replace(/\\.$/, '');\n for (const pattern of allowedHosts) {\n if (pattern === '*') {\n return true;\n }\n if (pattern.startsWith('*.')) {\n const suffix = pattern.slice(1);\n if (host.endsWith(suffix) && host.length > suffix.length) {\n return true;\n }\n } else if (host === pattern) {\n return true;\n }\n }\n return false;\n}\n\nexport function hostRefusal(hostname: string): Response {\n return new Response(\n `Host \"${hostname}\" is not allowed by ngx-cloudflare. ` +\n `Add it to ALLOWED_HOSTS in the vars of wrangler.jsonc, or to allowedHosts in src/server.ts. ` +\n `localhost, 127.0.0.1 and *.workers.dev are allowed by default.`,\n { status: 400, headers: { 'content-type': 'text/plain; charset=utf-8' } },\n );\n}\n\nexport function isProxyHeader(name: string): boolean {\n const lower = name.toLowerCase();\n return lower === 'forwarded' || lower.startsWith('x-forwarded-');\n}\n\nexport function stripProxyHeaders(request: Request): Request {\n const names: string[] = [];\n request.headers.forEach((_value, name) => {\n if (isProxyHeader(name)) {\n names.push(name);\n }\n });\n if (names.length === 0) {\n return request;\n }\n const headers = new Headers(request.headers);\n for (const name of names) {\n headers.delete(name);\n }\n return new Request(request, { headers });\n}\n\nexport function toApiHandler<Env>(api: ApiOption<Env> | undefined): ApiHandler<Env> | undefined {\n if (!api) {\n return undefined;\n }\n if (typeof api === 'function') {\n return api;\n }\n return (request, env, ctx) => api.fetch(request, env, ctx);\n}\n\nfunction matchesPrefix(request: Request, prefix: string | false): boolean {\n if (prefix === false) {\n return true;\n }\n const { pathname } = new URL(request.url);\n const bare = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;\n return pathname === bare || pathname.startsWith(bare + '/');\n}\n\nexport async function callInProcess<Env>(\n handler: ApiHandler<Env>,\n request: Request,\n env: Env,\n ctx: CloudflareExecutionContext,\n): Promise<Response | null> {\n inProcessRequests.add(request);\n return (await handler(request, env, ctx)) ?? null;\n}\n\nconst NOOP_CONTEXT: CloudflareExecutionContext = {\n waitUntil() {},\n passThroughOnException() {},\n};\n\nexport function createCloudflareFetch<Env = Cloudflare.Env>(\n render: RenderFunction,\n options: CloudflareHandlerOptions<Env> = {},\n loadDevPlatform?: DevPlatformLoader,\n): CloudflareFetchHandler<Env> {\n const configuredHosts = options.allowedHosts ?? [];\n const hostsByEnv = new WeakMap<object, string[]>();\n const allowedHostsFor = (env: unknown): string[] => {\n if (typeof env !== 'object' || env === null) {\n return resolveAllowedHosts(configuredHosts);\n }\n let hosts = hostsByEnv.get(env);\n if (!hosts) {\n hosts = resolveAllowedHosts([...configuredHosts, ...hostsFromEnv(env)]);\n hostsByEnv.set(env, hosts);\n }\n return hosts;\n };\n const api = toApiHandler(options.api);\n const apiPrefix = options.apiPrefix ?? DEFAULT_API_PREFIX;\n const strip = options.stripProxyHeaders ?? true;\n const loggedRefusals = new Set<string>();\n\n async function callApi(\n request: Request,\n platform: CloudflarePlatform<Env> | undefined,\n ): Promise<Response | null> {\n if (!api || !matchesPrefix(request, apiPrefix)) {\n return null;\n }\n const result = await api(request, platform?.env as Env, platform?.ctx ?? NOOP_CONTEXT);\n return result ?? null;\n }\n\n async function serveAsset(\n request: Request,\n platform: CloudflarePlatform<Env> | undefined,\n ): Promise<Response | null> {\n const assets = (\n platform?.env as { ASSETS?: { fetch(request: Request): Promise<Response> } } | undefined\n )?.ASSETS;\n if (\n !assets ||\n typeof assets.fetch !== 'function' ||\n (request.method !== 'GET' && request.method !== 'HEAD')\n ) {\n return null;\n }\n const response = await assets.fetch(request);\n return response.status === 404 ? null : response;\n }\n\n function createContext(\n platform: CloudflarePlatform<Env>,\n source: CloudflareContextSource,\n ): CloudflareContext<Env> {\n return {\n env: platform.env,\n ctx: platform.ctx,\n cf: platform.cf,\n source,\n dispatch: async (request) => {\n inProcessRequests.add(request);\n return (await callApi(request, platform)) ?? (await serveAsset(request, platform));\n },\n [CLOUDFLARE_CONTEXT_MARKER]: true,\n };\n }\n\n return async (request, env, ctx) => {\n let platform: CloudflarePlatform<Env> | undefined;\n let source: CloudflareContextSource = 'workers';\n if (env !== undefined) {\n platform = {\n env,\n ctx: ctx ?? NOOP_CONTEXT,\n cf: (request as Request & { cf?: CloudflareRequestCf }).cf,\n };\n } else if (loadDevPlatform) {\n platform = await loadDevPlatform();\n source = 'platform-proxy';\n }\n\n const { hostname } = new URL(request.url);\n const allowedHosts = allowedHostsFor(platform?.env);\n if (!isHostAllowed(hostname, allowedHosts)) {\n if (!loggedRefusals.has(hostname)) {\n if (loggedRefusals.size >= MAX_LOGGED_REFUSALS) {\n loggedRefusals.clear();\n }\n loggedRefusals.add(hostname);\n console.error(\n `ngx-cloudflare: refused host \"${hostname}\". Allowed hosts: ${allowedHosts.join(', ')}`,\n );\n }\n return hostRefusal(hostname);\n }\n\n const apiResponse = await callApi(request, platform);\n if (apiResponse) {\n return apiResponse;\n }\n\n const context = platform ? createContext(platform, source) : undefined;\n const response = await render(strip ? stripProxyHeaders(request) : request, context);\n return response ?? new Response('Page not found.', { status: 404 });\n };\n}\n","import { AngularAppEngine, createRequestHandler } from '@angular/ssr';\nimport {\n CloudflareFetchHandler,\n CloudflareHandlerOptions,\n CloudflarePlatform,\n createCloudflareFetch,\n hostsFromEnv,\n resolveAllowedHosts,\n} from './cloudflare-fetch';\n\ndeclare const ngDevMode: boolean | undefined;\n\nexport interface DevPlatformOptions {\n configPath?: string;\n environment?: string;\n persist?: boolean | { path: string };\n}\n\nexport interface CloudflareRequestHandlerOptions<\n Env = Cloudflare.Env,\n> extends CloudflareHandlerOptions<Env> {\n devPlatform?: DevPlatformOptions | false;\n}\n\ninterface ProxyCache {\n __ngxCloudflarePlatformProxy?: Promise<CloudflarePlatform<any, any> | undefined>;\n}\n\nfunction loadPlatformProxy(\n options: DevPlatformOptions,\n): Promise<CloudflarePlatform<any, any> | undefined> {\n const cache = globalThis as ProxyCache;\n cache.__ngxCloudflarePlatformProxy ??= import('wrangler')\n .then((wrangler) => wrangler.getPlatformProxy(options))\n .then((proxy) => ({ env: proxy.env, ctx: proxy.ctx, cf: proxy.cf }))\n .catch((error: unknown) => {\n cache.__ngxCloudflarePlatformProxy = undefined;\n console.error(\n \"ngx-cloudflare: could not start wrangler's platform proxy, so bindings are unavailable in ng serve. \" +\n 'Install wrangler as a dev dependency and check wrangler.jsonc. The next request tries again.',\n error,\n );\n return undefined;\n });\n return cache.__ngxCloudflarePlatformProxy;\n}\n\nexport function createCloudflareRequestHandler<Env = Cloudflare.Env>(\n options: CloudflareRequestHandlerOptions<Env> = {},\n): CloudflareFetchHandler<Env> {\n let engine: AngularAppEngine | undefined;\n const devPlatform = options.devPlatform;\n\n const handler = createCloudflareFetch<Env>(\n (request, context) => {\n engine ??= new AngularAppEngine({\n allowedHosts: resolveAllowedHosts([\n ...(options.allowedHosts ?? []),\n ...hostsFromEnv(context?.env),\n ]),\n });\n return engine.handle(request, context);\n },\n options,\n () => {\n if ((typeof ngDevMode === 'undefined' || ngDevMode) && devPlatform !== false) {\n return loadPlatformProxy(devPlatform ?? {});\n }\n return undefined;\n },\n );\n\n createRequestHandler(handler as (request: Request) => Promise<Response>);\n return handler;\n}\n"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { REQUEST, REQUEST_CONTEXT, makeEnvironmentProviders } from "@angular/core";
2
+ import { CLOUDFLARE_CONTEXT_MARKER, callInProcess, toApiHandler } from "ngx-cloudflare/server";
3
+ function createTestExecutionContext(overrides = {}) {
4
+ const waitUntilPromises = [];
5
+ return {
6
+ waitUntilPromises,
7
+ waitUntil: (promise) => {
8
+ waitUntilPromises.push(promise);
9
+ },
10
+ passThroughOnException: () => {},
11
+ ...overrides
12
+ };
13
+ }
14
+ function createCloudflareTestContext(options = {}) {
15
+ const env = options.env ?? {};
16
+ const ctx = createTestExecutionContext(options.ctx);
17
+ const handler = toApiHandler(options.api);
18
+ return {
19
+ env,
20
+ ctx,
21
+ cf: options.cf,
22
+ source: options.source ?? "workers",
23
+ dispatch: async (request) => handler ? callInProcess(handler, request, env, ctx) : null,
24
+ [CLOUDFLARE_CONTEXT_MARKER]: true
25
+ };
26
+ }
27
+ function provideCloudflareTesting(options = {}) {
28
+ return makeEnvironmentProviders([{
29
+ provide: REQUEST_CONTEXT,
30
+ useValue: createCloudflareTestContext(options)
31
+ }, {
32
+ provide: REQUEST,
33
+ useValue: new Request(options.url ?? "http://localhost/")
34
+ }]);
35
+ }
36
+ export { createCloudflareTestContext, createTestExecutionContext, provideCloudflareTesting };
37
+
38
+ //# sourceMappingURL=ngx-cloudflare-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-cloudflare-testing.mjs","names":[],"sources":["../../../projects/ngx-cloudflare/testing/src/provide-cloudflare-testing.ts"],"sourcesContent":["import {\n EnvironmentProviders,\n makeEnvironmentProviders,\n REQUEST,\n REQUEST_CONTEXT,\n} from '@angular/core';\nimport {\n ApiOption,\n callInProcess,\n CLOUDFLARE_CONTEXT_MARKER,\n type CloudflareContext,\n type CloudflareContextSource,\n type CloudflareExecutionContext,\n type CloudflareRequestCf,\n toApiHandler,\n} from 'ngx-cloudflare/server';\n\nexport interface CloudflareTestingOptions<Env = Cloudflare.Env, Cf = CloudflareRequestCf> {\n env?: Partial<Env>;\n cf?: Cf;\n ctx?: Partial<CloudflareExecutionContext>;\n url?: string;\n api?: ApiOption<Env>;\n source?: CloudflareContextSource;\n}\n\nexport interface TestExecutionContext extends CloudflareExecutionContext {\n readonly waitUntilPromises: Promise<unknown>[];\n}\n\nexport function createTestExecutionContext(\n overrides: Partial<CloudflareExecutionContext> = {},\n): TestExecutionContext {\n const waitUntilPromises: Promise<unknown>[] = [];\n return {\n waitUntilPromises,\n waitUntil: (promise) => {\n waitUntilPromises.push(promise);\n },\n passThroughOnException: () => {},\n ...overrides,\n };\n}\n\nexport function createCloudflareTestContext<Env = Cloudflare.Env, Cf = CloudflareRequestCf>(\n options: CloudflareTestingOptions<Env, Cf> = {},\n): CloudflareContext<Env, Cf> {\n const env = (options.env ?? {}) as Env;\n const ctx = createTestExecutionContext(options.ctx);\n const handler = toApiHandler(options.api);\n return {\n env,\n ctx,\n cf: options.cf,\n source: options.source ?? 'workers',\n dispatch: async (request) => (handler ? callInProcess(handler, request, env, ctx) : null),\n [CLOUDFLARE_CONTEXT_MARKER]: true,\n };\n}\n\nexport function provideCloudflareTesting<Env = Cloudflare.Env, Cf = CloudflareRequestCf>(\n options: CloudflareTestingOptions<Env, Cf> = {},\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: REQUEST_CONTEXT, useValue: createCloudflareTestContext(options) },\n { provide: REQUEST, useValue: new Request(options.url ?? 'http://localhost/') },\n ]);\n}\n"],"mappings":";;AA8BA,SAAgB,2BACd,YAAiD,CAAA,GAAE;CAEnD,MAAM,oBAAwC,CAAA;CAC9C,OAAO;EACL;EACA,YAAY,YAAW;GACrB,kBAAkB,KAAK,OAAO;EAChC;EACA,8BAA6B,CAAE;EAC/B,GAAG;CACJ;AACH;AAEA,SAAgB,4BACd,UAA6C,CAAA,GAAE;CAE/C,MAAM,MAAO,QAAQ,OAAO,CAAA;CAC5B,MAAM,MAAM,2BAA2B,QAAQ,GAAG;CAClD,MAAM,UAAU,aAAa,QAAQ,GAAG;CACxC,OAAO;EACL;EACA;EACA,IAAI,QAAQ;EACZ,QAAQ,QAAQ,UAAU;EAC1B,UAAU,OAAO,YAAa,UAAU,cAAc,SAAS,SAAS,KAAK,GAAG,IAAI;GACnF,4BAA4B;CAC9B;AACH;AAEA,SAAgB,yBACd,UAA6C,CAAA,GAAE;CAE/C,OAAO,yBAAyB,CAC9B;EAAE,SAAS;EAAiB,UAAU,4BAA4B,OAAO;CAAC,GAC1E;EAAE,SAAS;EAAS,UAAU,IAAI,QAAQ,QAAQ,OAAO,mBAAmB;CAAC,CAC9E,CAAA;AACH"}
@@ -0,0 +1,175 @@
1
+ import * as i0 from "@angular/core";
2
+ import { Injectable, REQUEST, REQUEST_CONTEXT, assertInInjectionContext, inject, makeEnvironmentProviders } from "@angular/core";
3
+ import { isCloudflareContext } from "ngx-cloudflare/server";
4
+ import { FetchBackend, HttpBackend, HttpErrorResponse, HttpEventType, HttpHeaders, HttpResponse } from "@angular/common/http";
5
+ import { Observable } from "rxjs";
6
+ const MISSING_CONTEXT = "ngx-cloudflare: no Cloudflare request context. Bindings exist only while a request is rendered on the server (on Workers or in ng serve). They are null in the browser, during prerendering and at build time. Check that src/server.ts uses createCloudflareRequestHandler(), or call this without { required: true } and handle null.";
7
+ function readContext(fn) {
8
+ assertInInjectionContext(fn);
9
+ const value = inject(REQUEST_CONTEXT, { optional: true });
10
+ return isCloudflareContext(value) ? value : null;
11
+ }
12
+ function orThrow(value, options) {
13
+ if (value === null && options?.required) throw new Error(MISSING_CONTEXT);
14
+ return value;
15
+ }
16
+ function injectCloudflare(options) {
17
+ return orThrow(readContext(injectCloudflare), options);
18
+ }
19
+ function injectEnv(options) {
20
+ return orThrow(readContext(injectEnv)?.env ?? null, options);
21
+ }
22
+ function injectExecutionContext(options) {
23
+ return orThrow(readContext(injectExecutionContext)?.ctx ?? null, options);
24
+ }
25
+ function injectRequestCf(options) {
26
+ return orThrow(readContext(injectRequestCf)?.cf ?? null, options);
27
+ }
28
+ const XSSI_PREFIX = /^\)\]\}',?\n/;
29
+ function toHeaders(headers) {
30
+ let result = new HttpHeaders();
31
+ headers.forEach((value, name) => {
32
+ result = result.append(name, value);
33
+ });
34
+ return result;
35
+ }
36
+ function toRequest(req, url, signal) {
37
+ const headers = new Headers();
38
+ for (const name of req.headers.keys()) for (const value of req.headers.getAll(name) ?? []) headers.append(name, value);
39
+ if (!headers.has("accept")) headers.set("accept", "application/json, text/plain, */*");
40
+ const body = req.serializeBody();
41
+ if (body !== null && !headers.has("content-type")) {
42
+ const detected = req.detectContentTypeHeader();
43
+ if (detected !== null) headers.set("content-type", detected);
44
+ }
45
+ const hasBody = body !== null && req.method !== "GET" && req.method !== "HEAD";
46
+ return new Request(url, {
47
+ method: req.method,
48
+ headers,
49
+ body: hasBody ? body : void 0,
50
+ signal
51
+ });
52
+ }
53
+ async function readBody(req, res) {
54
+ switch (req.responseType) {
55
+ case "arraybuffer": return { body: await res.arrayBuffer() };
56
+ case "blob": return { body: await res.blob() };
57
+ case "text": return { body: await res.text() };
58
+ default: {
59
+ const text = await res.text();
60
+ if (text === "") return { body: null };
61
+ try {
62
+ return { body: JSON.parse(text.replace(XSSI_PREFIX, "")) };
63
+ } catch (parseError) {
64
+ return {
65
+ body: text,
66
+ parseError
67
+ };
68
+ }
69
+ }
70
+ }
71
+ }
72
+ var CloudflareFetchBackend = class CloudflareFetchBackend extends FetchBackend {
73
+ cloudflare = inject(REQUEST_CONTEXT, { optional: true });
74
+ incoming = inject(REQUEST, { optional: true });
75
+ handle(req) {
76
+ const context = this.cloudflare;
77
+ if (!this.incoming || !isCloudflareContext(context)) return super.handle(req);
78
+ const base = new URL(this.incoming.url);
79
+ const url = new URL(req.urlWithParams, base);
80
+ if (url.origin !== base.origin) return super.handle(req);
81
+ return new Observable((observer) => {
82
+ const aborter = new AbortController();
83
+ let phase = { kind: "dispatching" };
84
+ const settle = (next) => {
85
+ if (phase.kind !== "dispatching") return false;
86
+ phase = next;
87
+ return true;
88
+ };
89
+ (async () => {
90
+ const response = await context.dispatch(toRequest(req, url, aborter.signal));
91
+ if (phase.kind !== "dispatching") return;
92
+ if (!response) {
93
+ const subscription = super.handle(req).subscribe(observer);
94
+ if (!settle({
95
+ kind: "fallback",
96
+ subscription
97
+ })) subscription.unsubscribe();
98
+ return;
99
+ }
100
+ observer.next({ type: HttpEventType.Sent });
101
+ const headers = toHeaders(response.headers);
102
+ const { body, parseError } = await readBody(req, response);
103
+ if (!settle({ kind: "done" })) return;
104
+ if (response.status >= 200 && response.status < 300 && !parseError) {
105
+ observer.next(new HttpResponse({
106
+ body,
107
+ headers,
108
+ status: response.status,
109
+ statusText: response.statusText,
110
+ url: url.href
111
+ }));
112
+ observer.complete();
113
+ } else observer.error(new HttpErrorResponse({
114
+ error: parseError ? {
115
+ error: parseError,
116
+ text: body
117
+ } : body,
118
+ headers,
119
+ status: response.status,
120
+ statusText: response.statusText,
121
+ url: url.href
122
+ }));
123
+ })().catch((error) => {
124
+ if (settle({ kind: "done" })) observer.error(new HttpErrorResponse({
125
+ error,
126
+ status: 0,
127
+ statusText: "Unknown Error",
128
+ url: url.href
129
+ }));
130
+ });
131
+ return () => {
132
+ if (phase.kind === "fallback") phase.subscription.unsubscribe();
133
+ else if (settle({ kind: "cancelled" })) aborter.abort();
134
+ };
135
+ });
136
+ }
137
+ static ɵfac = i0.ɵɵngDeclareFactory({
138
+ minVersion: "12.0.0",
139
+ version: "22.2.5",
140
+ ngImport: i0,
141
+ type: CloudflareFetchBackend,
142
+ deps: null,
143
+ target: i0.ɵɵFactoryTarget.Injectable
144
+ });
145
+ static ɵprov = i0.ɵɵngDeclareInjectable({
146
+ minVersion: "12.0.0",
147
+ version: "22.2.5",
148
+ ngImport: i0,
149
+ type: CloudflareFetchBackend
150
+ });
151
+ };
152
+ i0.ɵɵngDeclareClassMetadata({
153
+ minVersion: "12.0.0",
154
+ version: "22.2.5",
155
+ ngImport: i0,
156
+ type: CloudflareFetchBackend,
157
+ decorators: [{ type: Injectable }]
158
+ });
159
+ function provideCloudflare(options = {}) {
160
+ if (options.inProcessRequests === false) return makeEnvironmentProviders([]);
161
+ return makeEnvironmentProviders([
162
+ CloudflareFetchBackend,
163
+ {
164
+ provide: FetchBackend,
165
+ useExisting: CloudflareFetchBackend
166
+ },
167
+ {
168
+ provide: HttpBackend,
169
+ useExisting: CloudflareFetchBackend
170
+ }
171
+ ]);
172
+ }
173
+ export { CloudflareFetchBackend, injectCloudflare, injectEnv, injectExecutionContext, injectRequestCf, provideCloudflare };
174
+
175
+ //# sourceMappingURL=ngx-cloudflare.mjs.map