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.
@@ -0,0 +1,3 @@
1
+ # Copy to .dev.vars for `npm run dev`
2
+ ORIGIN=https://example.pages.dev
3
+ BASE_PATH=/landing-page
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
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
@@ -0,0 +1,9 @@
1
+ name = "digivalt-uworker"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2025-07-01"
4
+
5
+ # Set per environment / dashboard (do not commit secrets).
6
+ # Example:
7
+ # [vars]
8
+ # ORIGIN = "https://my-landing.pages.dev"
9
+ # BASE_PATH = "/landing-page"