@dytsou/shorten-url 2.4.4 → 2.4.6

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,308 @@
1
+ ---
2
+ title: "Worker-Hosted React Assets and Local Build - Plan"
3
+ type: refactor
4
+ date: 2026-09-06
5
+ topic: worker-hosted-react-assets
6
+ artifact_contract: ce-unified-plan/v1
7
+ artifact_readiness: implementation-ready
8
+ product_contract_source: ce-plan-bootstrap
9
+ execution: code
10
+ deepened: 2026-09-06
11
+ ---
12
+
13
+ # Worker-Hosted React Assets and Local Build - Plan
14
+
15
+ ## Goal Capsule
16
+
17
+ - **Objective:** Make the React frontend a reproducible Workers Static Assets build served directly by the URL-shortening Worker, so local development and production use the same `frontend/dist` asset path while preserving shortening, redirect, Flagship, and Access behavior.
18
+ - **Product authority:** The existing URL-shortening Worker remains the runtime authority. React/Vite remains the frontend source and build system; Worker logic remains under `src/`; the existing documentation site is not merged into the product frontend.
19
+ - **Open blockers:** Cloudflare account identifiers, KV/Flagship bindings, Access configuration, and deployment secrets still need to be supplied by the deployment environment. They are operational prerequisites, not reasons to make local compilation depend on live Cloudflare services.
20
+
21
+ ## Product Contract
22
+
23
+ ### Summary
24
+
25
+ Serve the main React UI and protected settings shell from a Workers Assets binding rooted at `frontend/dist`. A clean checkout must be able to build the frontend, start Wrangler, and exercise the real Worker route ordering without copying `docs/` or requiring a real Access/Flagship service locally.
26
+
27
+ React is retained. A VanillaJS rewrite is explicitly out of scope because the existing frontend already owns the UI composition, same-origin API configuration, credentials, and CSRF behavior required by this change.
28
+
29
+ ### Problem Frame
30
+
31
+ The current example Worker obtains the main page through `fetchHostedPage(getEndpoints().shortenPage)`, while the repository configuration points Wrangler at a missing `src/worker.js` and has no Workers Assets binding. The frontend build is a separate Vite package, and the root scripts do not guarantee that `frontend/dist` exists before `wrangler dev` or deployment.
32
+
33
+ That split creates three failure modes: local development can pass with ignored developer files that are absent from a clean checkout; the Worker can serve a different page path from the React API configuration; and a broad SPA fallback can turn an unknown short-link key or API path into HTML instead of a redirect or JSON response.
34
+
35
+ ### Key Decisions
36
+
37
+ - **The Worker hosts the product frontend through Workers Assets** (session-settled: user-directed — chosen over a Pages proxy or embedding the frontend bundle into Worker JavaScript so local and production use one origin and one route boundary). Governs R1, R5, R8, and R11.
38
+ - **React/Vite remains the implementation boundary** (session-settled: user-approved — chosen over a VanillaJS rewrite because the existing UI and API helpers already provide the needed behavior, while a rewrite would add unrelated product risk). Governs R1, R2, and R7.
39
+ - **The root workflow builds first and then runs Wrangler** (session-settled: user-directed — chosen over treating Vite dev server or a separately hosted page as the integration path). Governs R2, R3, and R13.
40
+ - **Access remains enforced for local settings** (session-settled: user-directed — chosen over a local-only bypass or fake production authorization so local tests exercise the same protected boundary). Governs R6, R10, and R13.
41
+ - **Only `frontend/dist` is in the Worker asset collection** (session-settled: user-directed — `docs/index.html` and the rest of `docs/` remain outside the product asset boundary). Governs R1, R5, and R11.
42
+
43
+ ### Actors
44
+
45
+ - A1. **Visitor:** Opens the shortening UI, submits a destination, or follows a short link.
46
+ - A2. **Authorized operator:** Opens `/settings` and uses the existing protected Flagship management APIs.
47
+ - A3. **Local developer:** Builds the React package and runs the Worker with deterministic local mocks.
48
+ - A4. **Cloudflare runtime:** Provides Worker execution, KV, Access context, Flagship bindings, and Workers Assets.
49
+
50
+ ### Requirements
51
+
52
+ **Source and build boundary**
53
+
54
+ - R1. All product UI source and generated UI assets remain under `frontend/`; all Worker routing, asset delegation, compatibility, and backend behavior remain under `src/`. The implementation must not modify `docs/index.html` or copy `docs/` into the Worker asset directory.
55
+ - R2. A root build contract must compile the existing React/Vite package into exactly `frontend/dist`, with a clean checkout able to reproduce the output from declared dependencies. Generated `frontend/dist` remains untracked.
56
+ - R3. The root development path must build the frontend before starting `wrangler dev`. A separate Vite-only watch command may remain available for UI iteration, but it is not the end-to-end runtime contract.
57
+ - R4. Wrangler must point at a tracked, reproducible Worker entrypoint and an explicit Workers Assets configuration. Startup must not depend on ignored `src/worker.js`/`src/worker.dev.js` files or injected global configuration that only exists in one developer's shell.
58
+
59
+ **Runtime and routing**
60
+
61
+ - R5. The Worker must serve `/` and frontend static assets through an `ASSETS` binding rooted only at `frontend/dist`.
62
+ - R6. `GET`/`HEAD` `/settings`, `/settings/`, and future non-API `/settings/*` shell requests, plus every `/settings/api` and `/settings/api/*` request, must remain behind the existing Access authorization and CSRF/origin protections. Local development must use mocks or fixtures to exercise this boundary, never a production-auth bypass.
63
+ - R7. The React app must use same-origin URLs in Worker-hosted mode. `POST /shorten` is the canonical shortening endpoint; existing `POST /` behavior is retained only as an explicitly tested compatibility path if current clients require it.
64
+ - R8. Dynamic routes must win over asset fallback: preflight, shortening APIs, settings APIs, Access checks, Flagship evaluation, and short-link redirects/not-found handling must execute before the Worker delegates to Assets. Unknown short-link paths and unknown API paths must not become the React shell.
65
+ - R9. The Worker-hosted shell for `/` and authorized `/settings` must be fetched from the built asset collection. The normal UI path must not fetch an externally hosted page. Legacy external error-page behavior may remain only when an explicit legacy frontend URL is configured; Worker-hosted mode must have a safe local response when it is not.
66
+ - R10. Local tests must provide deterministic Access, Flagship, KV, and Assets doubles. Production code must not contain a local-only authorization branch, and browser code must never receive Flagship credentials or Worker secrets.
67
+ - R15. The production route boundary must not trust client-supplied Access-like headers on an unprotected direct Worker hostname. Production must disable the default direct Worker hostname, expose the Worker only through an allowlisted custom hostname protected by Access, and reject protected requests whose host is not allowlisted; forged headers must not authorize settings, shortening, or Flagship evaluation.
68
+
69
+ **Deployment and maintenance**
70
+
71
+ - R11. Production deployment must use the same Worker Assets path as local integration. If GitHub Pages remains enabled for documentation, its artifact must be documentation-only; it must not remain the source of the product frontend.
72
+ - R12. Existing short-link KV behavior, redirect safety, Flagship evaluation/fallback semantics, response shapes, and OPTIONS behavior must remain unchanged except where the new asset route boundary necessarily replaces hosted-page fetching.
73
+ - R13. Verification must cover a clean build, the real Wrangler runtime, protected settings, same-origin shortening, static assets, short-link redirects, unknown paths, and local provider failure behavior.
74
+ - R14. Documentation and example configuration must explain the build-before-Wrangler workflow, the `frontend/dist` asset boundary, required production bindings/secrets, and the distinction between the product Worker and the documentation site.
75
+
76
+ ### Key Flows
77
+
78
+ - F1. **Build and start locally**
79
+ - **Trigger:** A developer checks out the repository and runs the root development workflow.
80
+ - **Actors:** A3, A4.
81
+ - **Steps:** The root workflow builds `frontend/` into `frontend/dist`, starts Wrangler with the tracked Worker entrypoint, and exposes the Worker origin.
82
+ - **Outcome:** `/` returns the built React shell from the Assets binding without requiring a real hosted frontend, Access tenant, or Flagship service.
83
+ - **Covered by:** R2, R3, R4, R5, R10, R13.
84
+
85
+ - F2. **Submit a shortening request**
86
+ - **Trigger:** A visitor submits the React form.
87
+ - **Actors:** A1, A4.
88
+ - **Steps:** React sends same-origin `POST /shorten`; the Worker recognizes the API before asset fallback and invokes the existing shortener.
89
+ - **Outcome:** The existing success, validation, storage, and error responses are returned as API responses rather than the React shell.
90
+ - **Covered by:** R7, R8, R12, R13.
91
+
92
+ - F3. **Follow a short link**
93
+ - **Trigger:** A visitor requests `/<key>`.
94
+ - **Actors:** A1, A4.
95
+ - **Steps:** The Worker classifies the path as a short-link candidate before delegating to Assets, evaluates any active Flagship behavior, and performs the existing safe redirect or not-found response.
96
+ - **Outcome:** A valid key redirects; an invalid key retains the existing not-found semantics; neither result is replaced by `index.html`.
97
+ - **Covered by:** R8, R9, R12, R13.
98
+
99
+ - F4. **Open protected settings**
100
+ - **Trigger:** An operator requests `/settings`.
101
+ - **Actors:** A2, A4.
102
+ - **Steps:** The Worker authorizes the request first, then fetches the React shell from the Assets binding. Subsequent `/settings/api` and `/settings/api/*` calls remain in the protected Flagship route boundary.
103
+ - **Outcome:** Authorized operators see the settings UI; unauthorized requests receive the existing denial behavior; API failures remain JSON/API failures.
104
+ - **Covered by:** R6, R8, R9, R10, R13.
105
+
106
+ - F5. **Deploy the same artifact shape**
107
+ - **Trigger:** The production deployment workflow runs.
108
+ - **Actors:** A4, repository automation.
109
+ - **Steps:** CI installs declared dependencies, builds `frontend/dist`, validates the Worker/Assets configuration, and deploys the Worker with the required production bindings and secrets.
110
+ - **Outcome:** Production serves the same Worker-hosted React route boundary; the documentation Pages artifact, if retained, contains only documentation.
111
+ - **Covered by:** R2, R4, R5, R11, R14.
112
+
113
+ ### Acceptance Examples
114
+
115
+ - AE1. **Clean checkout build:** Given a clean checkout with declared dependencies installed, when the root build runs, then `frontend/dist/index.html` and its Vite assets are produced and no generated asset is required to be committed.
116
+ - AE2. **Worker-hosted root:** Given the built asset directory and local Worker bindings, when `GET /` is requested through Wrangler, then the response is the React shell from `ASSETS`, not an external hosted-page fetch.
117
+ - AE3. **Protected settings shell:** Given no valid Access context, when `GET` or `HEAD` `/settings`, `/settings/`, or a supported settings deep link is requested, then the request is denied before the asset shell is returned. Given a valid mocked Access context, the shell is returned from `ASSETS`.
118
+ - AE4. **API precedence:** Given a built frontend, when `POST /shorten`, `POST /` compatibility, OPTIONS, or `/settings/api/*` is requested, then the appropriate existing Worker/API handler runs and no HTML shell is returned.
119
+ - AE5. **Redirect precedence:** Given a valid and invalid short-link key, when each key is requested, then the valid key keeps its redirect and the invalid key keeps its not-found response; neither request receives SPA HTML.
120
+ - AE6. **Same-origin frontend:** Given the Worker-hosted build with no explicit cross-origin override, when the React form or settings UI creates a request, then its URL is same-origin and credentials/CSRF behavior remains intact.
121
+ - AE7. **Local provider isolation:** Given local mock Access/Flagship/KV/Assets bindings, when settings and shortening flows are exercised, then tests are deterministic and no production secret or live provider request is required.
122
+ - AE8. **Deployment parity:** Given the production workflow, when the Worker is deployed, then its asset directory is `frontend/dist`, its entrypoint is tracked, and the Pages artifact does not provide the product frontend.
123
+ - AE9. **Documentation boundary:** Given the implementation, when the diff is inspected, then `docs/index.html` is unchanged and no file under `docs/` is copied into `frontend/dist` or the Worker asset collection.
124
+
125
+ ### Scope Boundaries
126
+
127
+ - **Deferred for later:** React visual redesign, new settings features, a VanillaJS rewrite, HMR through Wrangler, a new Flagship product model, and changes to country targeting or fallback semantics already covered by the existing Flagship plan.
128
+ - **Outside this product's identity:** The documentation site under `docs/` is not the product frontend and is not an alternate Worker asset source.
129
+ - **Compatibility boundary:** The existing external hosted-page path is not removed wholesale in this plan; it is removed from the normal Worker-hosted UI path and retained only for explicitly configured legacy error/compatibility behavior.
130
+
131
+ ### Dependencies and Assumptions
132
+
133
+ - Wrangler v4 supports the Workers Assets `directory`, `binding`, `run_worker_first`, and `not_found_handling` settings used by the plan.
134
+ - `frontend/` remains a separate Vite package rather than adopting the Cloudflare Vite plugin; its React plugin and Vite version remain declared in that package.
135
+ - Local Wrangler can provide KV and test bindings without a live Cloudflare account. Access and Flagship behavior is represented by test/mocked environments.
136
+ - Production continues to provide the existing `LINKS`, Access, Flagship, and observability configuration. Secret values are provisioned outside the repository.
137
+ - The current frontend routes are the root shell and `/settings`; adding another React navigation route later requires adding it to the Worker shell route table rather than enabling a global fallback that could capture short links.
138
+
139
+ ### Outstanding Questions
140
+
141
+ - **Resolved in Planning:** Create and track `src/worker.js` as the canonical Wrangler entrypoint. Extract the shared handler/config normalization into `src/lib/` so `src/worker.example.js` remains an executable example/fixture without being the only production source.
142
+ - **Resolved in Planning:** Configure a named `ASSETS` binding for `frontend/dist`, set Worker-first routing for the application boundary, and use `not_found_handling = "none"` with explicit shell delegation rather than broad automatic SPA fallback. This makes `/settings` authorization and short-link precedence testable and prevents unknown keys from becoming HTML.
143
+ - **Resolved in Planning:** Use `/` for the Worker-hosted shell and `/shorten` as the canonical frontend API path. Keep `POST /` only as a compatibility handler if existing clients/tests demonstrate that it is still required.
144
+ - **Resolved in Planning:** Protect `/settings` itself as well as `/settings/api/*`; an unauthenticated request must not receive the settings shell.
145
+ - **Resolved in Planning:** Protect `/settings`, `/settings/`, and all non-API `/settings/*` shell paths before any Assets fetch; match `/settings/api/*` first as the API branch.
146
+ - **Resolved in Planning:** Use a dedicated Worker deployment workflow for the product frontend and make the existing Pages workflow documentation-only if documentation deployment is still needed. The product deploy must not depend on the Pages artifact.
147
+ - **Resolved in Planning:** Treat Worker-hosted same-origin requests as the default credentialed origin. Any retained cross-origin mode requires an explicit allowlist; unknown, `null`, or missing mutation origins are rejected and wildcard credentials are forbidden. Production disables the default direct Worker hostname and accepts protected user-facing traffic only on an allowlisted Access-protected custom hostname.
148
+ - **Deferred to implementation:** Confirm the exact Cloudflare account name, Access issuer/audience values, Flagship binding names, and CI secret names in the deployment environment. These values must be wired through configuration/secrets, never guessed or committed.
149
+
150
+ ## Planning Contract
151
+
152
+ ### Key Technical Decisions
153
+
154
+ - **KTD1 — Workers Assets is the single product frontend boundary.** Configure one Assets collection at `frontend/dist` with an `ASSETS` binding and `not_found_handling = "none"`. The Worker explicitly delegates the root shell and known static assets through that binding. This follows the user’s direct-Worker decision and avoids a Pages proxy or bundle embedding; the official Assets binding supports `env.ASSETS.fetch()` for this pattern. Explicit shell mapping is safer than global SPA fallback because every non-reserved path is potentially a short-link key. Governs R1, R5, R8, R9, and R11.
155
+ - **KTD2 — Use Worker-first routing for correctness, then delegate cacheable assets.** Set `run_worker_first = true` for the Worker route boundary. The Worker handles OPTIONS, settings/API authorization, shortening APIs, root Flagship evaluation, and reserved static/shell paths before short-link resolution; it delegates only an explicitly recognized shell or static asset to `ASSETS`. This costs an invocation for the dynamic boundary but prevents authorization and redirect semantics from being decided by asset routing. Governs R6, R8, R9, and R12.
156
+ - **KTD3 — Establish one tracked runtime entrypoint and one route composer.** `src/worker.js` becomes the canonical `main`; a shared `src/lib/worker-handler.js` owns route composition and normalized runtime configuration; `src/worker.example.js` reuses that boundary for examples/tests. `flag-routes.js` receives a settings-shell responder or remains limited to authorization/API concerns and must not know about Workers Assets or call `fetchHostedPage()`. This removes the current clean-checkout gap and prevents entrypoint/UI transport drift. Governs R4, R6, R8, R10, and R13.
157
+ - **KTD4 — Keep the frontend package separate but make the root contract authoritative.** Preserve the existing React/Vite package and React plugin, add a root build that delegates to it, and make root development/deployment depend on that build. Keep a committed `frontend/pnpm-lock.yaml` for the separate package and use frozen installs in CI/deploy/local setup. Do not add the Cloudflare Vite plugin or rewrite the UI. Use root-relative asset URLs for the Worker origin and verify `/` and `/settings` explicitly. Governs R1, R2, R3, R7, and R13.
158
+ - **KTD5 — Treat same-origin as the supported hosted mode with separate path fields and an origin matrix.** Normalize `shellPath = "/"`, `settingsPath = "/settings"`, `shortenApiPath = "/shorten"`, and `flagEvaluationPath = "/"` independently; keep compatibility `POST /` separate rather than reusing the evaluation path. Existing API helper credentials and CSRF behavior remain in place. The Worker origin is the default allowed credentialed origin; a configured legacy frontend origin is an explicit opt-in, while unknown/`null`/missing mutation origins are rejected and `Access-Control-Allow-Origin: *` is never combined with credentials. External-origin configuration is not required for the Worker-hosted flow. Governs R7, R9, R12, R13, and R15.
159
+ - **KTD6 — Local realism comes from mocks, not weakened authorization.** Test environments provide Access claims/headers, Flagship behavior, KV, and Assets fetchers. The runtime has no `NODE_ENV`-style bypass for `/settings`; missing, malformed, or forged authorization remains denied in local and production code paths. Production uses an origin-lock policy: disable the default direct Worker hostname, allowlist the Access-protected custom hostname, and reject protected requests on any other host before relying on the existing header-based shortening/evaluation checks. Governs R6, R10, R13, and R15.
160
+ - **KTD7 — Public configuration is an allowlisted projection.** Only non-sensitive frontend values such as shell/API paths, public origin, and display settings may reach Vite metadata or generated HTML. Worker secrets, Access values, Flagship tokens/CSRF material, KV identifiers, test sentinels, local vars, and secret-bearing source maps/files must be excluded from `frontend/dist` and deployment manifests. Governs R2, R10, R14, and R15.
161
+ - **KTD8 — Separate product deployment from documentation deployment.** A dedicated Worker deployment workflow builds and uploads only `frontend/dist` from a tracked non-secret Wrangler configuration. The Pages workflow, if kept, builds/deploys `docs/` without copying the frontend. Production deploys require an explicit prerequisite gate, a pre-deploy manifest, a non-mutating smoke, and a previous Worker version plus asset manifest for rollback. This keeps `docs/index.html` untouched while preventing two production frontend authorities. Governs R1, R11, R13, and R14.
162
+
163
+ ### High-Level Technical Design
164
+
165
+ ```mermaid
166
+ flowchart TD
167
+ Source[frontend React/Vite source] --> Build[root build contract]
168
+ Build --> Dist[frontend/dist]
169
+ Dist --> Assets[Workers Assets binding]
170
+ Request[Browser request] --> Gate[Tracked Worker route gate]
171
+ Gate --> Options[OPTIONS / CORS]
172
+ Gate --> SettingsAuth[Access + CSRF boundary]
173
+ SettingsAuth --> SettingsAPI[/settings/api/*]
174
+ SettingsAuth --> SettingsShell[/settings shell]
175
+ Gate --> ShortenAPI[/shorten and compatibility POST]
176
+ Gate --> Flagship[Eligible Flagship evaluation]
177
+ Flagship --> Redirect[Short-link redirect or not-found]
178
+ Gate --> Static[Known static asset]
179
+ Gate --> Root[Root shell]
180
+ SettingsShell --> Assets
181
+ Static --> Assets
182
+ Root --> Assets
183
+ ShortenAPI --> KV[Existing LINKS / shortener]
184
+ Redirect --> KV
185
+ Deploy[Worker deploy workflow] --> Build
186
+ Deploy --> Assets
187
+ ```
188
+
189
+ The route gate is intentionally ordered:
190
+
191
+ | Priority | Request class | Result |
192
+ | --- | --- | --- |
193
+ | 1 | OPTIONS and explicit CORS preflight | Existing 204/preflight behavior |
194
+ | 2 | `/settings/api` and `/settings/api/*` | Access, origin, CSRF, then Flagship management/API response |
195
+ | 3 | `/settings`, `/settings/`, and future non-API settings shell paths | Access first, then map the shell request to the root asset |
196
+ | 4 | Canonical shortening API and any retained compatibility POST | Existing shortener response |
197
+ | 5 | Eligible root/shortening-page evaluation | Existing Flagship decision and safe fallback |
198
+ | 6 | `/` root shell | Explicit root `ASSETS` fetch after evaluation/fallback |
199
+ | 7 | `/assets/*` and an explicit allowlist of root public assets | Static asset `ASSETS` fetch |
200
+ | 8 | Short-link candidate paths | Existing redirect/not-found/safety behavior; never automatic SPA HTML |
201
+ | 9 | Unknown API/static paths | Existing JSON/404 semantics; no shell fallback |
202
+
203
+ The asset helper should fetch the built root shell for `/settings` rather than asking the asset collection to reinterpret an arbitrary path. It should preserve request method, headers, content type, and cache behavior for static files, while never forwarding Access-sensitive settings responses through a public asset path. The normalized route configuration must keep shell, shortening API, and Flagship evaluation paths independent so changing one cannot silently move another.
204
+
205
+ ### Implementation Units
206
+
207
+ #### U1. Establish the tracked Worker runtime and Assets configuration
208
+
209
+ - **Files:** `src/worker.js` (create), `src/worker.example.js`, the new `src/lib/worker-handler.js` and runtime-config module, `wrangler.toml.example`, and related Worker configuration documentation.
210
+ - **Depends on:** Existing `src/worker.example.js` flow, `src/lib/flag-routes.js`, `src/lib/responses.js`, `src/lib/endpoints.js`, and the current ignored local Wrangler configuration.
211
+ - **Approach:** Extract the shared request composition without changing existing shortener/Flagship semantics. Make `src/worker.js` the tracked `main`, normalize configuration from Worker bindings/variables with safe local defaults, and keep example/test adapters on the same shared path. Keep `flag-routes.js` limited to settings authorization/API and inject the shell response from the route composer. Add an `ASSETS` binding rooted at `frontend/dist`, `run_worker_first = true`, and `not_found_handling = "none"`; keep the local `wrangler.toml` developer copy ignored but make `wrangler.toml.example` sufficient to reproduce the shape. The production deployment profile must disable the default direct Worker hostname and declare the allowlisted Access-protected route; local Wrangler may use localhost without that production route. Do not use global `importConfig`/`defaultConfig` as the only startup contract.
212
+ - **Test scenarios:** Import the canonical entrypoint in a clean test environment; verify missing optional local provider bindings do not prevent the Worker from starting; verify the example and canonical adapters share route behavior; verify shell/API/evaluation path fields are independent; verify the Wrangler example names `src/worker.js`, `frontend/dist`, `ASSETS`, `run_worker_first`, `not_found_handling`, and the existing KV binding; verify a protected-route request on a non-allowlisted host is rejected even with forged Access-like headers.
213
+
214
+ #### U2. Make the root build contract produce the Worker asset directory
215
+
216
+ - **Files:** Root `package.json`, `frontend/package.json`, new `frontend/pnpm-lock.yaml`, `frontend/vite.config.js`, `frontend/index.html`, `.github/workflows/ci.yml`, and package-manager lock/config files needed for reproducibility.
217
+ - **Depends on:** U1’s asset directory and tracked entrypoint.
218
+ - **Approach:** Add one root build entry that invokes the existing frontend package and one root development/deployment path that runs that build before Wrangler. Keep Vite and React as the frontend toolchain, set the build base for the Worker origin, and make the generated API metadata agree with `/`, `/settings`, and `/shorten`. Keep the frontend build output ignored, use the committed frontend lockfile with frozen installs, and avoid adding a second frontend bundler or Cloudflare-specific plugin.
219
+ - **Test scenarios:** Build from a clean checkout using the root and frontend frozen dependency contracts; assert `frontend/dist/index.html` references generated assets with paths that work at `/` and `/settings`; confirm a failed frontend build prevents Wrangler from starting; confirm repeated builds do not change tracked source; verify CI uses the same root build contract rather than a separately assembled frontend artifact.
220
+
221
+ #### U3. Replace hosted-page UI fetches with route-safe asset delegation
222
+
223
+ - **Files:** `src/worker.js`, `src/worker.example.js`, the shared Worker application module from U1, `src/lib/flag-routes.js`, `src/lib/responses.js`, `src/lib/endpoints.js`, `frontend/src/api.js`, and `frontend/index.html`.
224
+ - **Depends on:** U1 and U2.
225
+ - **Approach:** Add a narrow asset/shell delegation boundary. `/` and authorized `/settings` obtain the shell from `env.ASSETS`; known generated assets are delegated directly. Protect `/settings`, `/settings/`, and future non-API `/settings/*` before shell mapping, with `/settings/api` and `/settings/api/*` matched first. Apply the same authorization before `HEAD` shell responses. Enforce the allowlisted production host before any header-presence access check on shortening/evaluation paths. Preserve the existing API, redirect, Flagship evaluation, CORS, CSRF, and error behavior before delegation. Remove `fetchHostedPage` from normal product-page serving, and split Worker-hosted endpoint data from optional legacy `frontend.url` data so missing legacy configuration produces a safe local error/not-found response rather than startup failure or an unintended external fetch. Any retained legacy URL must be an explicit validated HTTPS endpoint, and legacy HTML/interstitial output must retain existing escaping guarantees. Make same-origin frontend configuration the default and use independent shell/API/evaluation paths with a tested compatibility decision for `POST /`. Do not construct filesystem paths from request strings; delegate through `ASSETS.fetch()` only.
226
+ - **Test scenarios:** Mock `env.ASSETS.fetch` and verify root HTML/static content types; verify `GET`/`HEAD` `/settings`, `/settings/`, and a settings deep link deny before asset fetch and serve the shell after mocked authorization; verify `/settings/api`, `/settings/api/*`, `POST /shorten`, OPTIONS, valid redirects, invalid keys, unknown APIs, malformed/encoded asset paths, hidden files, and Flagship fallback never return the shell or call `ASSETS.fetch` unexpectedly; verify same-origin URL and CSRF/credentials behavior in `tests/frontend-api.test.js`; verify `javascript:`, `data:`, protocol-relative, credential-bearing, and malformed variant destinations are rejected before redirect.
227
+
228
+ #### U4. Add deterministic local Access/Flagship/Assets integration fixtures
229
+
230
+ - **Files:** `tests/mocks/cloudflare-workers.js`, new focused worker asset/routing fixtures under `tests/`, `tests/worker-flagship.test.js`, `tests/flag-routes.test.js`, and any shared test setup needed to stop relying on ignored local Worker files.
231
+ - **Depends on:** U3’s route boundary and existing Flagship test fixtures.
232
+ - **Approach:** Extend the current mocks with an Assets fetcher that can return the built shell, static files, and misses; keep Access mocks explicit per test; keep Flagship management/evaluation failures injectable. Test both local success and failure states without changing production authorization. Use tracked fixtures or in-memory responses so a clean CI checkout has the same evidence as a developer machine. Add negative fixtures for forged Access-like headers, provider exceptions containing secret-like text, and public-config leak sentinels.
233
+ - **Test scenarios:** Missing/invalid/forged Access denies `/settings`, settings API mutations, and any protected route; authorized verified mocked Access reaches the shell/API; missing Flagship binding, provider failure, malformed decision, and no-match preserve current shortening behavior; missing asset returns the intended error; provider error responses/log fields do not contain raw token/secret text; the built asset inventory contains only allowlisted public config and no `.dev.vars`, `config.js`, secret-like filenames, or test sentinels; no test makes a live provider request or exposes a secret.
234
+
235
+ #### U5. Align production deployment, documentation, and clean-checkout verification
236
+
237
+ - **Files:** Dedicated Worker deployment workflow, the existing `.github/workflows/deploy.yml` reduced to documentation-only if retained, `.github/workflows/ci.yml`, `README.md`, `wrangler.toml.example`, deployment/config examples, and no changes to `docs/index.html`.
238
+ - **Depends on:** U1-U4.
239
+ - **Approach:** Build `frontend/dist` in the dedicated Worker deployment job and deploy the tracked Worker with environment-provided Cloudflare credentials/bindings. Production configuration disables the default direct Worker hostname and binds the Worker to an allowlisted Access-protected custom hostname. PR jobs only build, test, and dry-run; production deploys run from a protected environment on the repository’s release trigger or manual approval. Reduce Pages to documentation-only; move any documentation CNAME handling to the documentation artifact rather than depending on `frontend/public/CNAME`; do not copy `frontend/dist` into `site/` as the product deployment. Add prerequisite checks for `LINKS`, `ASSETS`, Access, Flagship, allowed host, and deploy credentials; record the commit, public asset manifest, Worker version, and previous rollback target before deploy. Document local installation/build/dev, the ignored local Wrangler config, required production secrets, the direct-hostname/origin policy, and the exact frontend/backend ownership boundary.
240
+ - **Test scenarios:** CI installs both packages with frozen lockfiles, runs the root build and backend tests, and validates the Wrangler configuration without requiring production secrets in pull requests; deployment configuration points only to `frontend/dist`; a clean checkout can reproduce the Worker entrypoint and asset directory; a diff check confirms `docs/index.html` remains unchanged; a non-mutating post-deploy smoke checks root/static/settings/API/redirect/not-found behavior, forged-header behavior on the direct hostname, and logs for asset misses, binding failures, uncaught exceptions, HTML responses from API paths, and secret leakage. Rollback verification restores the matching Worker version and asset manifest before re-running the same smoke matrix.
241
+
242
+ ### System-Wide Impact
243
+
244
+ - **Runtime:** The Worker becomes the owner of the product UI origin. Existing shortener, redirect, Flagship, OPTIONS, and settings API handlers remain in `src/` and execute before asset delegation.
245
+ - **Frontend:** `frontend/src/api.js` and build metadata become same-origin by default. The root shell and settings shell share one compiled asset set; React source and Vite package boundaries remain intact.
246
+ - **Security:** `/settings` and `/settings/api/*` are protected before Assets. No public asset request can bypass the Worker authorization decision, and no local mode skips Access checks.
247
+ - **Security:** `/settings`, `/settings/`, and future non-API settings paths are protected before Assets. The deployment policy must also prevent direct-hostname header spoofing for protected user-facing routes; same-origin/legacy-origin CORS and CSRF rules are explicit rather than wildcarded.
248
+ - **Storage:** `LINKS` KV and Flagship authority are unchanged. The asset build is generated output only and must not become a second source of persisted state.
249
+ - **Deployment:** Worker deploy uploads `frontend/dist`; documentation deployment is separate. Production account IDs, Access values, Flagship identifiers, and secrets stay outside tracked files.
250
+ - **Performance:** `run_worker_first` prioritizes route correctness and may invoke the Worker for more requests; hashed Vite assets are still served through the Assets collection and can retain edge caching. A later optimization can narrow patterns only after route coverage is proven.
251
+ - **Compatibility:** Legacy `frontend.url` support is limited to explicitly configured non-primary error/compatibility paths. The normal UI no longer depends on an external page being reachable.
252
+ - **Rollout:** No production switch occurs until the build/entrypoint/config/asset manifest gates pass. Smoke requests are read-only and use a canary hostname or known canary link; they must not mutate KV or Flagship. Keep the prior Worker version and matching asset manifest available for rollback, then repeat the route/content-type matrix after rollback.
253
+
254
+ ### Risks and Dependencies
255
+
256
+ - **Entrypoint drift:** An ignored local Worker can make local tests pass while CI fails. Mitigation: track `src/worker.js`, point the example Wrangler config to it, import it in focused tests, and verify from a clean checkout.
257
+ - **Short-link capture by SPA behavior:** Automatic `index.html` fallback can mask invalid keys. Mitigation: use explicit shell delegation and test unknown keys/API paths before any asset fetch; do not rely on a global SPA fallback for this application.
258
+ - **Settings authorization bypass:** Asset-first routing could return the settings shell without Access. Mitigation: Worker-first route configuration and an authorization assertion before every settings shell/API response.
259
+ - **Static asset versus slug ambiguity:** A root-level favicon/manifest or future generated file can resemble a short key. Mitigation: make the asset classifier explicit and cover the actual Vite output plus a short-link miss in the same integration suite.
260
+ - **Build-path drift:** `frontend/` has its own package and currently uses a different Vite version from the root package. Mitigation: keep the package boundary deliberate, declare/install its dependencies reproducibly, and make CI invoke the same root build path developers use.
261
+ - **Duplicate production authorities:** Pages can continue to expose an old copy of the frontend. Mitigation: keep Pages documentation-only or remove its frontend assembly and add a Worker deploy path with a dry-run configuration gate.
262
+ - **Legacy hosted error pages:** Removing the external page entirely could change existing not-found/error behavior. Mitigation: retain opt-in legacy fetching where configured and provide a safe Worker-hosted fallback when it is not.
263
+ - **Direct-origin header spoofing:** The current access helper has a weaker header-presence path for some shortening/evaluation behavior. Mitigation: block direct Worker hostnames in production or require verified Access claims for all protected user-facing routes; add forged-header tests and a direct-hostname smoke gate.
264
+ - **Cross-origin credential confusion:** Retained external-origin mode could accept an unintended origin or wildcard credentials. Mitigation: enforce the origin matrix in KTD5, reject `null`/missing/unknown mutation origins, and test preflight plus credentialed mutation responses.
265
+ - **Public-config leakage:** Broad config normalization or source maps could place tokens, local vars, or test values in `frontend/dist`. Mitigation: use an allowlisted public projection, inventory generated assets, and add a sentinel denial test.
266
+ - **Provider error leakage:** Raw Flagship exception text could expose tokens or internal URLs through responses/logs. Mitigation: preserve generalized management errors, redact evaluation diagnostics, and assert redaction in provider-failure fixtures.
267
+ - **Unsafe variant redirect:** A mock or future provider adapter could return an unvalidated destination. Mitigation: validate the final destination at the redirect boundary and cover unsafe schemes, credentials, malformed URLs, and interstitial escaping.
268
+ - **Operational secrets:** Cloudflare and Flagship credentials must not enter Vite metadata, generated HTML, logs, tests, or workflow command output. Mitigation: use Worker/GitHub secret bindings and test configuration shape rather than secret values.
269
+
270
+ ### Verification Contract
271
+
272
+ - **Build:** The root build produces `frontend/dist/index.html` and all referenced assets; the generated directory is ignored; root development and deployment cannot skip the frontend build.
273
+ - **Worker startup:** The canonical tracked entrypoint loads with the example Wrangler configuration and exposes the `ASSETS` binding shape without an ignored local source file.
274
+ - **Asset serving:** `/` returns the built React shell; known JS/CSS/static files return their generated content types; `/settings`, `/settings/`, and supported settings deep links return the same shell only after Access authorization; malformed/hidden/missing asset paths do not expose files or silently become the shell.
275
+ - **Route precedence:** OPTIONS, `/shorten`, compatibility POST `/` when retained, `/settings/api/*`, Flagship evaluation, valid redirects, invalid keys, and unknown APIs preserve their existing non-HTML response semantics. Assert that API/redirect paths do not call `ASSETS.fetch` and that only shell/static paths do.
276
+ - **Security:** Missing, malformed, forged, or direct-hostname Access context cannot fetch `/settings` or mutate settings APIs. CSRF, origin, credential, redirect safety, error redaction, public-config projection, and secret-handling behavior remain enforced. Credentialed responses never use wildcard origins.
277
+ - **Local isolation:** Mocks cover Access, Flagship, KV, and Assets success/failure paths; tests do not need a live Cloudflare account or provider credentials.
278
+ - **Deployment:** Wrangler dry-run/config validation sees `main = src/worker.js`, the Assets directory `frontend/dist`, the `ASSETS` binding, the existing KV binding, and no `docs/` asset input. CI builds the same artifact before deploy. Production prerequisites, public asset manifest, previous rollback target, post-deploy smoke, and fixed observation-window evidence are recorded.
279
+ - **Regression:** Existing Flagship fallback, safe redirects, CORS/preflight, response shapes, and documentation content remain stable. `docs/index.html` has no implementation diff.
280
+ - **Parity:** The canonical Worker and example adapter pass the same route matrix for root shell, settings protection, shortening, redirect, unknown path, and provider failure scenarios.
281
+
282
+ ### Definition of Done
283
+
284
+ - A clean checkout can install dependencies, build React into `frontend/dist`, and start the tracked Worker through the root development workflow.
285
+ - Wrangler configuration exposes one `ASSETS` binding rooted at `frontend/dist`; no `docs/` content is uploaded as product frontend assets.
286
+ - `/` and authorized `/settings` are served by the Worker-hosted React shell; settings APIs, shortening, Flagship evaluation, redirects, and not-found behavior retain their route precedence and security boundaries.
287
+ - Local tests use deterministic Access/Flagship/KV/Assets mocks and include both success and failure paths without a production-auth bypass.
288
+ - CI and production deployment use the same build/asset shape; any retained Pages deployment is documentation-only.
289
+ - Production has a protected Worker deployment workflow with prerequisite checks, dry-run evidence, non-mutating post-deploy smoke evidence, and a known rollback Worker version plus matching asset manifest.
290
+ - `docs/index.html` is unchanged, frontend changes are confined to `frontend/`, backend/runtime changes are confined to `src/`, and supporting configuration/workflow/docs changes are documented.
291
+ - The full Verification Contract passes, including a real local Wrangler smoke flow and a Worker configuration dry run.
292
+
293
+ ### Sources / Research
294
+
295
+ - `src/worker.example.js` — current example handler order, hosted-page fetch, shortening POST, Flagship evaluation, and redirect dispatch.
296
+ - `src/lib/flag-routes.js` — settings authorization/API boundary and shortening evaluation entry point that must remain ahead of Assets.
297
+ - `src/lib/responses.js` — current hosted-page fallback, CORS, JSON, error, and not-found response semantics.
298
+ - `src/lib/endpoints.js` — current `frontend.url`-derived page endpoints and the compatibility surface for legacy hosted errors.
299
+ - `frontend/src/api.js` — existing same-origin defaults, credentials, CSRF header, and settings/shortening path configuration.
300
+ - `frontend/index.html` and `frontend/vite.config.js` — React entrypoint, current metadata defaults, and Vite asset base behavior.
301
+ - `package.json` and `frontend/package.json` — current separate root/frontend package boundaries, Node/pnpm requirements, and available Wrangler/Vite scripts.
302
+ - `wrangler.toml.example` and `.gitignore` — current missing Assets configuration, missing tracked Worker entrypoint, and ignored local runtime files.
303
+ - `.github/workflows/ci.yml` and `.github/workflows/deploy.yml` — current separate frontend build, Pages assembly, and the deployment split to reconcile.
304
+ - [Cloudflare Workers Static Assets configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) — authoritative `assets.directory`, `binding`, `run_worker_first`, and `not_found_handling` configuration shape.
305
+ - [Cloudflare Assets binding](https://developers.cloudflare.com/workers/static-assets/binding/) — authoritative `env.ASSETS.fetch()` runtime boundary and Worker-first behavior.
306
+ - [Cloudflare SPA routing](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/) — explains why automatic SPA fallback must be constrained for an application where arbitrary paths are short-link candidates.
307
+ - [Cloudflare Worker script routing](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/) — documents Worker-first routing tradeoffs and selective routing options.
308
+ - [Vite build guide](https://vite.dev/guide/build.html) — frontend build-output contract used by the root build workflow.
@@ -0,0 +1,124 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta
7
+ name="description"
8
+ content="How to shorten URLs and the complete Shorten URL project guide."
9
+ />
10
+ <title>Shorten URL Docs</title>
11
+ <style>
12
+ body {
13
+ max-width: 56rem;
14
+ margin: 2rem auto;
15
+ padding: 0 1.25rem;
16
+ color: #202923;
17
+ font:
18
+ 1rem/1.6 system-ui,
19
+ sans-serif;
20
+ }
21
+
22
+ a {
23
+ color: #137f60;
24
+ }
25
+
26
+ nav {
27
+ display: flex;
28
+ gap: 1rem;
29
+ margin-bottom: 1.5rem;
30
+ }
31
+
32
+ h1,
33
+ h2,
34
+ h3 {
35
+ line-height: 1.3;
36
+ }
37
+
38
+ h1 {
39
+ font-size: 2rem;
40
+ }
41
+
42
+ h2 {
43
+ margin-top: 2rem;
44
+ padding-top: 1rem;
45
+ border-top: 1px solid #d6ded9;
46
+ }
47
+
48
+ li + li {
49
+ margin-top: 0.4rem;
50
+ }
51
+
52
+ code,
53
+ pre {
54
+ background: #f3f6f4;
55
+ }
56
+
57
+ code {
58
+ padding: 0.1rem 0.25rem;
59
+ border-radius: 3px;
60
+ }
61
+
62
+ pre {
63
+ overflow-x: auto;
64
+ padding: 1rem;
65
+ border: 1px solid #d6ded9;
66
+ border-radius: 4px;
67
+ }
68
+
69
+ pre code {
70
+ padding: 0;
71
+ background: transparent;
72
+ }
73
+
74
+ table {
75
+ display: block;
76
+ overflow-x: auto;
77
+ border-collapse: collapse;
78
+ }
79
+
80
+ th,
81
+ td {
82
+ padding: 0.45rem 0.65rem;
83
+ border: 1px solid #d6ded9;
84
+ text-align: left;
85
+ vertical-align: top;
86
+ }
87
+
88
+ th {
89
+ background: #f3f6f4;
90
+ }
91
+
92
+ img {
93
+ max-width: 100%;
94
+ }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <main>
99
+ <nav aria-label="Site links">
100
+ <a href="/">Home</a>
101
+ <a href="/api/">API</a>
102
+ </nav>
103
+ <h1>How to use Shorten URL</h1>
104
+ <p>Create a short link from a destination URL in a few steps.</p>
105
+ <ol>
106
+ <li>
107
+ Enter the full destination URL, including <code>https://</code> or <code>http://</code>.
108
+ </li>
109
+ <li>
110
+ Optionally enter a custom slug. The URL prefix shown beside the field is part of your
111
+ short link. Leave the field blank to generate a short key automatically.
112
+ </li>
113
+ <li>Select <strong>Shorten URL</strong>.</li>
114
+ <li>
115
+ Use <strong>Copy link</strong> to copy the result, or <strong>Visit</strong> to open it.
116
+ </li>
117
+ </ol>
118
+ <nav aria-label="Documentation sections">
119
+ <a href="#project-documentation">Complete project guide</a>
120
+ </nav>
121
+ <article id="project-documentation"><!-- ABOUT_CONTENT --></article>
122
+ </main>
123
+ </body>
124
+ </html>
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="shorten-url-worker-origin" content="" />
7
+ <meta name="shorten-url-settings-path" content="/settings" />
8
+ <meta name="shorten-url-shorten-path" content="/" />
9
+ <meta name="theme-color" content="#111c1d" />
10
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
11
+ <title>Shorten URL — Link desk</title>
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.jsx"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@dytsou/shorten-url-frontend",
3
+ "private": true,
4
+ "version": "2.4.2",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.7.0",
17
+ "marked": "18.0.14",
18
+ "vite": "^6.4.3"
19
+ }
20
+ }