ngx-cloudflare 0.0.0-stage → 0.2.0
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 +21 -0
- package/README.md +175 -2
- package/fesm2022/ngx-cloudflare-server.mjs +174 -0
- package/fesm2022/ngx-cloudflare-server.mjs.map +1 -0
- package/fesm2022/ngx-cloudflare-testing.mjs +38 -0
- package/fesm2022/ngx-cloudflare-testing.mjs.map +1 -0
- package/fesm2022/ngx-cloudflare.mjs +175 -0
- package/fesm2022/ngx-cloudflare.mjs.map +1 -0
- package/package.json +65 -3
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/add-provider.js +54 -0
- package/schematics/ng-add/index.js +376 -0
- package/schematics/ng-add/schema.json +22 -0
- package/schematics/ng-add/server-entry.js +74 -0
- package/schematics/ng-add/wrangler-config.js +119 -0
- package/schematics/package.json +3 -0
- package/types/ngx-cloudflare-server.d.ts +61 -0
- package/types/ngx-cloudflare-testing.d.ts +16 -0
- package/types/ngx-cloudflare.d.ts +36 -0
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
|
-
|
|
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
|
-
|
|
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
|
+
> Version 0.2.0 is not on npm yet. Function names may still change before it is.
|
|
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
|