digivalt-uworker 0.1.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/.dev.vars.example +3 -0
- package/.gitattributes +2 -0
- package/CHANGELOG.md +14 -0
- package/README.md +63 -0
- package/package.json +15 -0
- package/src/index.ts +170 -0
- package/tsconfig.json +14 -0
- package/wrangler.toml +9 -0
package/.gitattributes
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog – digivalt-uworker
|
|
2
|
+
|
|
3
|
+
High-level log of notable work for this Worker.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 2026-07
|
|
8
|
+
|
|
9
|
+
### v0.1.0 — initial path proxy
|
|
10
|
+
|
|
11
|
+
- Configurable `ORIGIN` + `BASE_PATH` reverse proxy to DigiValt Cloudflare Pages.
|
|
12
|
+
- Strips public base path on upstream requests; rewrites `Location` and light HTML `href`/`src`/`action`.
|
|
13
|
+
- Pass-through for Pages Functions (`/api/*`, sitemap, static assets) after path mapping.
|
|
14
|
+
- Docs: consumer Vite `base` + router `basename` + digivalt-core `withBasePath` checklist.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# digivalt-uworker
|
|
2
|
+
|
|
3
|
+
Thin Cloudflare Worker that mounts a **DigiValt Cloudflare Pages** origin under a path on another hostname (typically a public WordPress site).
|
|
4
|
+
|
|
5
|
+
Example: DigiValt on `https://my-landing.pages.dev` served at `https://domain.tld/landing-page`.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Cloudflare Pages custom domains are hostname-only. This worker adds path-based publishing without migrating DigiValt to Workers.
|
|
10
|
+
|
|
11
|
+
## Environment
|
|
12
|
+
|
|
13
|
+
| Variable | Example | Notes |
|
|
14
|
+
|----------|---------|--------|
|
|
15
|
+
| `ORIGIN` | `https://my-landing.pages.dev` | Pages origin, no trailing slash |
|
|
16
|
+
| `BASE_PATH` | `/landing-page` | Public path, leading slash, no trailing slash |
|
|
17
|
+
|
|
18
|
+
Set via Wrangler `[vars]`, dashboard, or `.dev.vars` locally (see `.dev.vars.example`).
|
|
19
|
+
|
|
20
|
+
## DigiValt consumer checklist
|
|
21
|
+
|
|
22
|
+
The worker strips/prefixes paths and lightly rewrites HTML `href`/`src`/`action`. It does **not** fix React Router or Vite asset URLs inside JS bundles. The DigiValt app must be built for the same path:
|
|
23
|
+
|
|
24
|
+
1. Vite: `base: '/landing-page/'` (e.g. from `VITE_BASE_PATH`)
|
|
25
|
+
2. React Router: `BrowserRouter basename="/landing-page"`
|
|
26
|
+
3. SEO: `VITE_SEO_SITE_URL=https://domain.tld/landing-page`
|
|
27
|
+
4. Forms / Pages Functions: use `@marvalt/digivalt-core` **≥ 0.2.27** so `/api/*` calls use `import.meta.env.BASE_URL` (`withBasePath`)
|
|
28
|
+
|
|
29
|
+
One Pages build = one base path. Visiting the Pages apex (`*.pages.dev/`) will look blank; `*.pages.dev/landing-page/` usually works. Treat Pages as a private origin.
|
|
30
|
+
|
|
31
|
+
`digivalt-landing` stays root-hosted; use a separate DigiValt project for path mounts.
|
|
32
|
+
|
|
33
|
+
## Cloudflare route
|
|
34
|
+
|
|
35
|
+
On the customer zone (proxied):
|
|
36
|
+
|
|
37
|
+
- Route: `domain.tld/landing-page*` → this Worker
|
|
38
|
+
- Apex / other paths → WordPress (or existing origin)
|
|
39
|
+
|
|
40
|
+
## Develop
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cp .dev.vars.example .dev.vars
|
|
44
|
+
# edit ORIGIN + BASE_PATH
|
|
45
|
+
npm install
|
|
46
|
+
npm run dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Open `http://127.0.0.1:8787/landing-page` (port may vary).
|
|
50
|
+
|
|
51
|
+
## Deploy
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install
|
|
55
|
+
npx wrangler secret / vars # or set [vars] in wrangler.toml per env
|
|
56
|
+
npm run deploy
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then attach the zone route in the Cloudflare dashboard (or `routes` in `wrangler.toml`).
|
|
60
|
+
|
|
61
|
+
## Changelog
|
|
62
|
+
|
|
63
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "digivalt-uworker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal Cloudflare Worker: mount a DigiValt Pages origin under a path prefix",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "wrangler dev",
|
|
7
|
+
"deploy": "wrangler deploy",
|
|
8
|
+
"types": "wrangler types"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/workers-types": "^5.20260714.1",
|
|
12
|
+
"typescript": "^5.8.3",
|
|
13
|
+
"wrangler": "^4.24.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digivalt-uworker — reverse-proxy a DigiValt (Cloudflare Pages) origin under a path.
|
|
3
|
+
*
|
|
4
|
+
* Env:
|
|
5
|
+
* ORIGIN — Pages origin, no trailing slash (e.g. https://my-app.pages.dev)
|
|
6
|
+
* BASE_PATH — public path prefix, leading slash, no trailing slash (e.g. /landing-page)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Env {
|
|
10
|
+
ORIGIN: string;
|
|
11
|
+
BASE_PATH: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const HOP_BY_HOP = new Set([
|
|
15
|
+
"connection",
|
|
16
|
+
"keep-alive",
|
|
17
|
+
"proxy-authenticate",
|
|
18
|
+
"proxy-authorization",
|
|
19
|
+
"te",
|
|
20
|
+
"trailers",
|
|
21
|
+
"transfer-encoding",
|
|
22
|
+
"upgrade",
|
|
23
|
+
"host",
|
|
24
|
+
"cf-connecting-ip",
|
|
25
|
+
"cf-ray",
|
|
26
|
+
"cf-visitor",
|
|
27
|
+
"cf-ipcountry",
|
|
28
|
+
"x-forwarded-proto",
|
|
29
|
+
"x-forwarded-for",
|
|
30
|
+
"x-real-ip",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function normalizeBasePath(raw: string): string {
|
|
34
|
+
let p = (raw || "").trim();
|
|
35
|
+
if (!p) return "";
|
|
36
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
37
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeOrigin(raw: string): string {
|
|
42
|
+
return (raw || "").trim().replace(/\/+$/, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Map public pathname to origin pathname, or null if outside BASE_PATH. */
|
|
46
|
+
function toOriginPath(pathname: string, basePath: string): string | null {
|
|
47
|
+
if (pathname === basePath) return "/";
|
|
48
|
+
if (pathname.startsWith(`${basePath}/`)) {
|
|
49
|
+
const rest = pathname.slice(basePath.length);
|
|
50
|
+
return rest || "/";
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function filterRequestHeaders(src: Headers, originHost: string): Headers {
|
|
56
|
+
const out = new Headers();
|
|
57
|
+
src.forEach((value, key) => {
|
|
58
|
+
if (HOP_BY_HOP.has(key.toLowerCase())) return;
|
|
59
|
+
out.set(key, value);
|
|
60
|
+
});
|
|
61
|
+
out.set("Host", originHost);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function rewriteLocation(
|
|
66
|
+
location: string,
|
|
67
|
+
requestUrl: URL,
|
|
68
|
+
origin: string,
|
|
69
|
+
basePath: string,
|
|
70
|
+
): string {
|
|
71
|
+
try {
|
|
72
|
+
const resolved = new URL(location, origin);
|
|
73
|
+
const originUrl = new URL(origin);
|
|
74
|
+
if (resolved.origin !== originUrl.origin) {
|
|
75
|
+
return location;
|
|
76
|
+
}
|
|
77
|
+
const prefixed = `${basePath}${resolved.pathname === "/" ? "" : resolved.pathname}${resolved.search}${resolved.hash}`;
|
|
78
|
+
return new URL(prefixed || basePath, requestUrl.origin).toString();
|
|
79
|
+
} catch {
|
|
80
|
+
return location;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Light HTML rewrite: root-absolute href/src/action under the public base path. */
|
|
85
|
+
function rewriteHtml(html: string, basePath: string): string {
|
|
86
|
+
const prefix = (path: string) => {
|
|
87
|
+
if (path.startsWith("//") || path.startsWith("http:") || path.startsWith("https:")) {
|
|
88
|
+
return path;
|
|
89
|
+
}
|
|
90
|
+
if (path === basePath || path.startsWith(`${basePath}/`)) {
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
if (path.startsWith("/")) {
|
|
94
|
+
return `${basePath}${path}`;
|
|
95
|
+
}
|
|
96
|
+
return path;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return html.replace(
|
|
100
|
+
/\b(href|src|action)=(["'])(\/[^"']*)\2/gi,
|
|
101
|
+
(_m, attr: string, quote: string, path: string) =>
|
|
102
|
+
`${attr}=${quote}${prefix(path)}${quote}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function proxy(request: Request, env: Env): Promise<Response> {
|
|
107
|
+
const origin = normalizeOrigin(env.ORIGIN);
|
|
108
|
+
const basePath = normalizeBasePath(env.BASE_PATH);
|
|
109
|
+
|
|
110
|
+
if (!origin || !basePath) {
|
|
111
|
+
return new Response("digivalt-uworker: ORIGIN and BASE_PATH must be set", { status: 500 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const reqUrl = new URL(request.url);
|
|
115
|
+
const originPath = toOriginPath(reqUrl.pathname, basePath);
|
|
116
|
+
if (originPath === null) {
|
|
117
|
+
return new Response("Not Found", { status: 404 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const originUrl = new URL(origin);
|
|
121
|
+
const target = new URL(originPath + reqUrl.search, origin);
|
|
122
|
+
|
|
123
|
+
const headers = filterRequestHeaders(request.headers, originUrl.host);
|
|
124
|
+
const init: RequestInit & { duplex?: "half" } = {
|
|
125
|
+
method: request.method,
|
|
126
|
+
headers,
|
|
127
|
+
redirect: "manual",
|
|
128
|
+
};
|
|
129
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
130
|
+
init.body = request.body;
|
|
131
|
+
init.duplex = "half";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const upstream = await fetch(target.toString(), init);
|
|
135
|
+
const outHeaders = new Headers(upstream.headers);
|
|
136
|
+
|
|
137
|
+
const loc = outHeaders.get("Location");
|
|
138
|
+
if (loc) {
|
|
139
|
+
outHeaders.set("Location", rewriteLocation(loc, reqUrl, origin, basePath));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const contentType = outHeaders.get("Content-Type") || "";
|
|
143
|
+
if (contentType.includes("text/html") && request.method === "GET") {
|
|
144
|
+
const html = await upstream.text();
|
|
145
|
+
const rewritten = rewriteHtml(html, basePath);
|
|
146
|
+
outHeaders.delete("content-length");
|
|
147
|
+
return new Response(rewritten, {
|
|
148
|
+
status: upstream.status,
|
|
149
|
+
statusText: upstream.statusText,
|
|
150
|
+
headers: outHeaders,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new Response(upstream.body, {
|
|
155
|
+
status: upstream.status,
|
|
156
|
+
statusText: upstream.statusText,
|
|
157
|
+
headers: outHeaders,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default {
|
|
162
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
163
|
+
try {
|
|
164
|
+
return await proxy(request, env);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
167
|
+
return new Response(`digivalt-uworker proxy error: ${message}`, { status: 502 });
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|
package/wrangler.toml
ADDED