@startup-api/cloudflare 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +114 -0
  3. package/package.json +53 -0
  4. package/public/index.html +405 -0
  5. package/public/users/accounts.html +504 -0
  6. package/public/users/admin/index.html +765 -0
  7. package/public/users/power-strip.js +658 -0
  8. package/public/users/profile.html +443 -0
  9. package/public/users/style.css +493 -0
  10. package/src/CookieManager.ts +56 -0
  11. package/src/PowerStrip.ts +23 -0
  12. package/src/StartupAPIEnv.ts +12 -0
  13. package/src/auth/GoogleProvider.ts +67 -0
  14. package/src/auth/OAuthProvider.ts +52 -0
  15. package/src/auth/TwitchProvider.ts +64 -0
  16. package/src/auth/index.ts +231 -0
  17. package/src/billing/PaymentEngine.ts +20 -0
  18. package/src/billing/Plan.ts +80 -0
  19. package/src/billing/plansConfig.ts +48 -0
  20. package/src/handlers/account.ts +246 -0
  21. package/src/handlers/admin.ts +144 -0
  22. package/src/handlers/auth.ts +54 -0
  23. package/src/handlers/ssr.ts +274 -0
  24. package/src/handlers/user.ts +168 -0
  25. package/src/handlers/utils.ts +120 -0
  26. package/src/index.ts +190 -0
  27. package/src/schemas/account.ts +37 -0
  28. package/src/schemas/admin.ts +10 -0
  29. package/src/schemas/billing.ts +11 -0
  30. package/src/schemas/credential.ts +38 -0
  31. package/src/schemas/membership.ts +9 -0
  32. package/src/schemas/session.ts +10 -0
  33. package/src/schemas/user.ts +22 -0
  34. package/src/storage/AccountDO.ts +370 -0
  35. package/src/storage/CredentialDO.ts +82 -0
  36. package/src/storage/SystemDO.ts +264 -0
  37. package/src/storage/UserDO.ts +385 -0
  38. package/worker-configuration.d.ts +11696 -0
  39. package/wrangler.template.jsonc +55 -0
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2026 Sergey Chernyshev and contributors.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Startup API Cloudflare App
2
+
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+
5
+ This application uses the Cloudflare Developer Platform, including Workers and DurableObjects, to implement foundational web application functionality. It acts as a transparent proxy for your application, allowing you to inject custom UI elements and intercept specific paths.
6
+
7
+ ## Features
8
+
9
+ - **Transparent Proxying:** Forwards requests to your origin application
10
+ - **HTML Injection:** Uses `HTMLRewriter` to inject scripts and custom elements (like `<power-strip>`) into your HTML pages
11
+ - **Path Interception:** Intercepts requests to a configurable path to serve internal assets
12
+
13
+ ## Installation
14
+
15
+ ### Option 1: Cloudflare Workers GitHub Integration (Recommended)
16
+
17
+ This is the easiest way to deploy and keep your worker up to date.
18
+
19
+ 1. **Fork this repository** to your account
20
+ 2. Go to your [Cloudflare Dashboard's Workers & pages > Create Application](https://dash.cloudflare.com/?to=/:account/workers-and-pages/create)
21
+ 3. Click **Continue with GitHub**
22
+ 4. Select your forked `startup-api-cloudflare` repository
23
+ 5. Pick the name for your site's worker (e.g. you might have multiple)
24
+ 6. Deploy the Worker
25
+ 7. In the **Settings** tab of your Worker, go to **Variables** and add the required `ORIGIN_URL` (see [Configuration](#configuration-details) below)
26
+
27
+ ### Option 2: Manual Installation (CLI)
28
+
29
+ Use this option if you want to deploy from your local machine.
30
+
31
+ 1. **Clone and Install**
32
+ ```bash
33
+ git clone https://github.com/StartupAPI/startup-api-cloudflare.git
34
+ cd startup-api-cloudflare
35
+ npm install
36
+ ```
37
+ 2. **Configure Environment Variables**
38
+
39
+ Update `wrangler.jsonc` or use dashboard **Settings** tab of your Worker, go to **Variables** and add the required `ORIGIN_URL` (see [Configuration](#configuration-details) below)
40
+
41
+ 3. **Deploy**
42
+ ```bash
43
+ npm run deploy
44
+ ```
45
+
46
+ ## Configuration Details
47
+
48
+ ### How to set environment variables
49
+
50
+ - **Using Cloudflare Dashboard (Recommended):**
51
+ 1. Go to **Workers & Pages**
52
+ 2. Select your worker
53
+ 3. Navigate to **Settings** > **Variables**
54
+ 4. Click **Add variable** under **Environment Variables**
55
+ 5. Add `ORIGIN_URL` and any optional variables
56
+ 6. Click **Save and deploy**
57
+
58
+ - **Using `wrangler.jsonc`:**
59
+ Add the variables to the `"vars"` object in your configuration file. See [Cloudflare documentation](https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables) for more details.
60
+
61
+ | Variable | Required | Default | Description |
62
+ | :--------------------- | :------- | :-------- | :---------------------------------------------------------------------------- |
63
+ | `ORIGIN_URL` | **Yes** | N/A | The base URL of your origin application (e.g., `https://your-app-origin.com`) |
64
+ | `USERS_PATH` | No | `/users/` | The path used to serve internal assets like `power-strip.js` |
65
+ | `AUTH_ORIGIN` | No | N/A | Optional base URL for OAuth redirects (overrides request origin) |
66
+ | `GOOGLE_CLIENT_ID` | No | N/A | Google OAuth2 Client ID |
67
+ | `GOOGLE_CLIENT_SECRET` | No | N/A | Google OAuth2 Client Secret |
68
+ | `TWITCH_CLIENT_ID` | No | N/A | Twitch OAuth2 Client ID |
69
+ | `TWITCH_CLIENT_SECRET` | No | N/A | Twitch OAuth2 Client Secret |
70
+
71
+ ### Setting up OAuth
72
+
73
+ #### Google
74
+
75
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
76
+ 2. Create a new project or select an existing one
77
+ 3. Navigate to **APIs & Services > Credentials**
78
+ 4. Click **Create Credentials > OAuth client ID**
79
+ 5. Select **Web application** as the application type
80
+ 6. Add your authorized redirect URI: `https://<your-worker-url>/users/auth/google/callback`
81
+ 7. Copy the **Client ID** and **Client Secret** and add them to your Worker's environment variables
82
+
83
+ #### Twitch
84
+
85
+ 1. Go to the [Twitch Developer Console](https://dev.twitch.tv/console)
86
+ 2. Register a new application
87
+ 3. Add your authorized redirect URI: `https://<your-worker-url>/users/auth/twitch/callback`
88
+ 4. Select **Website** as the category
89
+ 5. Copy the **Client ID** and generate a **Client Secret** to add them to your Worker's environment variables
90
+
91
+ ### Example `wrangler.jsonc` snippet:
92
+
93
+ ```json
94
+ {
95
+ "vars": {
96
+ "ORIGIN_URL": "https://your-app-origin.com"
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## How It Works
102
+
103
+ 1. **Request Interception:** The worker receives all incoming requests
104
+ 2. **Path Mapping:** If the request path starts with `USERS_PATH`, the worker serves assets directly from the `public/users/` directory
105
+ 3. **Proxying:** All other requests are proxied to the configured `ORIGIN_URL`
106
+ 4. **Injection:** For `text/html` responses, the worker injects a `<script>` tag and a `<power-strip>` custom element before serving the content to the user
107
+
108
+ ## Contributing
109
+
110
+ Contributions are welcome! Please feel free to submit a Pull Request.
111
+
112
+ ## License
113
+
114
+ Licensed under the Apache License, Version 2.0. See [LICENSE](./LICENSE) for details.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@startup-api/cloudflare",
3
+ "version": "0.0.1",
4
+ "license": "Apache-2.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/StartupAPI/startup-api-cloudflare.git"
11
+ },
12
+ "main": "src/index.ts",
13
+ "types": "src/index.ts",
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./public/*": "./public/*",
17
+ "./wrangler.template.jsonc": "./wrangler.template.jsonc",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "public",
23
+ "worker-configuration.d.ts",
24
+ "wrangler.template.jsonc"
25
+ ],
26
+ "scripts": {
27
+ "deploy": "wrangler deploy",
28
+ "dev": "wrangler dev -c wrangler.local.jsonc",
29
+ "preview": "wrangler dev --env preview",
30
+ "test": "eslint . && vitest run",
31
+ "test:coverage": "vitest run --coverage --coverage.provider=istanbul",
32
+ "cf-typegen": "wrangler types",
33
+ "format": "npx prettier . --write",
34
+ "lint": "eslint .",
35
+ "lint:fix": "eslint . --fix"
36
+ },
37
+ "devDependencies": {
38
+ "@cloudflare/vitest-pool-workers": "^0.12.4",
39
+ "@eslint/js": "^10.0.1",
40
+ "@vitest/coverage-istanbul": "^3.2.4",
41
+ "@vitest/coverage-v8": "^3.2.4",
42
+ "eslint": "^10.0.1",
43
+ "globals": "^17.3.0",
44
+ "typescript": "^5.5.2",
45
+ "typescript-eslint": "^8.56.0",
46
+ "vitest": "~3.2.0",
47
+ "wrangler": "^4.60.0"
48
+ },
49
+ "dependencies": {
50
+ "prettier": "^3.8.1",
51
+ "zod": "^3.25.76"
52
+ }
53
+ }
@@ -0,0 +1,405 @@
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
+ <title>Startup API Cloudflare App - Documentation</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #f6821f; /* Cloudflare Orange */
10
+ --bg-color: #f8f9fa;
11
+ --text-color: #333;
12
+ --card-bg: #fff;
13
+ --card-border: #eee;
14
+ --pre-bg: #f1f1f1;
15
+ --nav-bg: #2c2c2c;
16
+ --hero-bg: linear-gradient(135deg, #2c2c2c 0%, #4a4a4a 100%);
17
+ --container-width: 800px;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root:not([data-theme='light']) {
22
+ --bg-color: #1a1a1a;
23
+ --text-color: #e0e0e0;
24
+ --card-bg: #2d2d2d;
25
+ --card-border: #444;
26
+ --pre-bg: #333;
27
+ --nav-bg: #111;
28
+ }
29
+ }
30
+
31
+ [data-theme='dark'] {
32
+ --bg-color: #1a1a1a;
33
+ --text-color: #e0e0e0;
34
+ --card-bg: #2d2d2d;
35
+ --card-border: #444;
36
+ --pre-bg: #333;
37
+ --nav-bg: #111;
38
+ }
39
+
40
+ [data-theme='light'] {
41
+ --bg-color: #f8f9fa;
42
+ --text-color: #333;
43
+ --card-bg: #fff;
44
+ --card-border: #eee;
45
+ --pre-bg: #f1f1f1;
46
+ --nav-bg: #2c2c2c;
47
+ }
48
+
49
+ body {
50
+ font-family:
51
+ system-ui,
52
+ -apple-system,
53
+ BlinkMacSystemFont,
54
+ 'Segoe UI',
55
+ Roboto,
56
+ Oxygen,
57
+ Ubuntu,
58
+ Cantarell,
59
+ 'Open Sans',
60
+ 'Helvetica Neue',
61
+ sans-serif;
62
+ line-height: 1.6;
63
+ color: var(--text-color);
64
+ background-color: var(--bg-color);
65
+ margin: 0;
66
+ transition:
67
+ background-color 0.3s,
68
+ color 0.3s;
69
+ }
70
+
71
+ nav {
72
+ background-color: var(--nav-bg);
73
+ color: white;
74
+ padding: 1rem 0;
75
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
76
+ }
77
+
78
+ .nav-container {
79
+ max-width: var(--container-width);
80
+ margin: 0 auto;
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ padding: 0 1rem;
85
+ }
86
+
87
+ .navbar-brand {
88
+ display: flex;
89
+ align-items: center;
90
+ font-weight: bold;
91
+ text-decoration: none;
92
+ color: white;
93
+ font-size: 1.25rem;
94
+ }
95
+
96
+ header.hero-section {
97
+ background: var(--hero-bg);
98
+ color: white;
99
+ padding: 4rem 1rem;
100
+ text-align: center;
101
+ margin-bottom: 2rem;
102
+ }
103
+
104
+ h1 {
105
+ margin: 0;
106
+ font-size: 2.5rem;
107
+ }
108
+
109
+ main {
110
+ max-width: var(--container-width);
111
+ margin: 0 auto;
112
+ padding: 0 1rem;
113
+ }
114
+
115
+ .card {
116
+ background-color: var(--card-bg);
117
+ border: 1px solid var(--card-border);
118
+ border-radius: 12px;
119
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
120
+ margin-bottom: 2rem;
121
+ overflow: hidden;
122
+ }
123
+
124
+ .card-header {
125
+ background-color: rgba(0, 0, 0, 0.03);
126
+ border-bottom: 1px solid var(--card-border);
127
+ font-weight: bold;
128
+ font-size: 1.2rem;
129
+ padding: 1rem 1.5rem;
130
+ }
131
+
132
+ .card-body {
133
+ padding: 1.5rem;
134
+ }
135
+
136
+ .variable-block {
137
+ margin-bottom: 1.5rem;
138
+ }
139
+
140
+ .variable-title {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 0.5rem;
144
+ margin-bottom: 0.5rem;
145
+ }
146
+
147
+ .variable-name {
148
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
149
+ color: var(--primary-color);
150
+ font-weight: bold;
151
+ background: rgba(246, 130, 31, 0.1);
152
+ padding: 0.2rem 0.4rem;
153
+ border-radius: 4px;
154
+ }
155
+
156
+ .badge-required {
157
+ background-color: #dc3545;
158
+ color: white;
159
+ font-size: 0.75rem;
160
+ padding: 0.2rem 0.5rem;
161
+ border-radius: 4px;
162
+ font-weight: bold;
163
+ }
164
+
165
+ pre {
166
+ background-color: var(--pre-bg);
167
+ color: var(--text-color);
168
+ padding: 1rem;
169
+ border-radius: 8px;
170
+ overflow-x: auto;
171
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
172
+ font-size: 0.9rem;
173
+ }
174
+
175
+ ol,
176
+ ul {
177
+ padding-left: 1.5rem;
178
+ }
179
+
180
+ li {
181
+ margin-bottom: 0.5rem;
182
+ }
183
+
184
+ footer {
185
+ text-align: center;
186
+ padding: 2rem 0;
187
+ color: #777;
188
+ border-top: 1px solid var(--card-border);
189
+ margin-top: 3rem;
190
+ }
191
+
192
+ .theme-toggle {
193
+ cursor: pointer;
194
+ background: transparent;
195
+ border: 1px solid rgba(255, 255, 255, 0.3);
196
+ color: white;
197
+ border-radius: 50%;
198
+ width: 40px;
199
+ height: 40px;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ transition: all 0.2s;
204
+ padding: 0;
205
+ }
206
+
207
+ .theme-toggle:hover {
208
+ background: rgba(255, 255, 255, 0.1);
209
+ }
210
+
211
+ .theme-toggle svg {
212
+ width: 20px;
213
+ height: 20px;
214
+ fill: currentColor;
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <nav>
220
+ <div class="nav-container">
221
+ <a class="navbar-brand" href="">
222
+ <svg
223
+ viewBox="0 0 24 24"
224
+ style="width: 24px; height: 24px; fill: #ffcc00; margin-right: 10px; filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 0.5))"
225
+ >
226
+ <path d="M7 2v11h3v9l7-12h-4l4-8z" />
227
+ </svg>
228
+ Startup API
229
+ </a>
230
+ <button class="theme-toggle" id="themeToggle" title="Toggle Theme">
231
+ <svg id="sunIcon" viewBox="0 0 24 24" style="display: none">
232
+ <path
233
+ d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 000-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 000-1.41.996.996 0 00-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 000-1.41.996.996 0 00-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"
234
+ />
235
+ </svg>
236
+ <svg id="moonIcon" viewBox="0 0 24 24">
237
+ <path
238
+ d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"
239
+ />
240
+ </svg>
241
+ </button>
242
+ </div>
243
+ </nav>
244
+
245
+ <script>
246
+ const themeToggle = document.getElementById('themeToggle');
247
+ const sunIcon = document.getElementById('sunIcon');
248
+ const moonIcon = document.getElementById('moonIcon');
249
+ const html = document.documentElement;
250
+
251
+ function updateIcons(theme) {
252
+ if (theme === 'dark') {
253
+ sunIcon.style.display = 'block';
254
+ moonIcon.style.display = 'none';
255
+ } else {
256
+ sunIcon.style.display = 'none';
257
+ moonIcon.style.display = 'block';
258
+ }
259
+ }
260
+
261
+ function setTheme(theme) {
262
+ if (theme) {
263
+ html.setAttribute('data-theme', theme);
264
+ updateIcons(theme);
265
+ } else {
266
+ html.removeAttribute('data-theme');
267
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
268
+ updateIcons(isDark ? 'dark' : 'light');
269
+ }
270
+ }
271
+
272
+ const savedTheme = localStorage.getItem('theme');
273
+ setTheme(savedTheme);
274
+
275
+ themeToggle.addEventListener('click', () => {
276
+ const currentTheme = html.getAttribute('data-theme');
277
+ let newTheme;
278
+
279
+ if (!currentTheme) {
280
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
281
+ newTheme = isDark ? 'light' : 'dark';
282
+ } else {
283
+ newTheme = currentTheme === 'dark' ? 'light' : 'dark';
284
+ }
285
+
286
+ setTheme(newTheme);
287
+ localStorage.setItem('theme', newTheme);
288
+ });
289
+
290
+ // Update icons if system theme changes and no manual override is set
291
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
292
+ if (!html.getAttribute('data-theme')) {
293
+ updateIcons(e.matches ? 'dark' : 'light');
294
+ }
295
+ });
296
+ </script>
297
+
298
+ <header class="hero-section">
299
+ <div class="container">
300
+ <h1 class="display-4">Welcome to Startup API</h1>
301
+ </div>
302
+ </header>
303
+
304
+ <main class="container">
305
+ <div class="row">
306
+ <div class="col-lg-8 offset-lg-2">
307
+ <!-- Overview -->
308
+
309
+ <section class="card">
310
+ <div class="card-header">Overview</div>
311
+
312
+ <div class="card-body">
313
+ <p>
314
+ This application uses Cloudflare Developer Platform, including Workers and DurableObjects to implement functionality that
315
+ every web application needs on day zero.
316
+ </p>
317
+
318
+ <p>
319
+ The worker acts as a transparent proxy for your application. When configured, it fetches content from your site, injects a
320
+ <code>&lt;power-strip&gt;</code>, and serves it to the user.
321
+ </p>
322
+ </div>
323
+ </section>
324
+
325
+ <!-- Configuration -->
326
+ <section class="card">
327
+ <div class="card-header">Required Configuration</div>
328
+ <div class="card-body">
329
+ <p>
330
+ These variables <strong>must</strong> be set in your <code>wrangler.jsonc</code> or Cloudflare Dashboard for the worker to
331
+ function. Until these are configured, the worker serves this documentation page.
332
+ </p>
333
+
334
+ <div class="mb-4">
335
+ <h5>
336
+ <span class="variable-name">ORIGIN_URL</span>
337
+ </h5>
338
+ <p>The base URL of the application you want to proxy, e.g. your app URL.</p>
339
+ <pre>ORIGIN_URL = "https://your-app-origin.com"</pre>
340
+ </div>
341
+
342
+ <div class="mb-4">
343
+ <h5>
344
+ <span class="variable-name">SESSION_SECRET</span>
345
+ </h5>
346
+ <p>A long, random string used to encrypt session cookies. <strong>Keep this secret!</strong></p>
347
+ <pre>SESSION_SECRET = "your-long-random-secret-string"</pre>
348
+ </div>
349
+ </div>
350
+ </section>
351
+
352
+ <section class="card">
353
+ <div class="card-header">Optional Configuration</div>
354
+ <div class="card-body">
355
+ <div class="mb-4">
356
+ <h5>
357
+ <span class="variable-name">USERS_PATH</span>
358
+ </h5>
359
+ <p>
360
+ The URL path where the worker intercepts requests to serve Startup API UI (like
361
+ <code>power-strip.js</code>).
362
+ </p>
363
+ <p><strong>Default:</strong> <code>/users/</code></p>
364
+ <pre>USERS_PATH = "/people/"</pre>
365
+ </div>
366
+ </div>
367
+ </section>
368
+
369
+ <!-- How it works -->
370
+ <section class="card">
371
+ <div class="card-header">How Injection Works</div>
372
+ <div class="card-body">
373
+ <ol>
374
+ <li>Worker receives a request.</li>
375
+ <li>
376
+ If the path starts with
377
+ <span class="variable-name">USERS_PATH</span>, it serves files from the <code>public/users/</code> directory.
378
+ </li>
379
+ <li>
380
+ Otherwise, it proxies the request to
381
+ <span class="variable-name">ORIGIN_URL</span>.
382
+ </li>
383
+ <li>
384
+ If the response is <code>text/html</code>, it injects:
385
+ <ul>
386
+ <li>A <code>&lt;script&gt;</code> tag loading <code>power-strip.js</code>.</li>
387
+ <li>A <code>&lt;power-strip&gt;</code> element styled to appear in the top-right corner.</li>
388
+ </ul>
389
+ </li>
390
+ </ol>
391
+ </div>
392
+ </section>
393
+ </div>
394
+ </div>
395
+ </main>
396
+
397
+ <footer>
398
+ <p>&copy; 2026 Startup API. Licensed under Apache 2.0.</p>
399
+
400
+ <p>
401
+ <a href="https://github.com/StartupAPI/cloudflare" style="color: var(--primary-color); text-decoration: none">View on GitHub</a>
402
+ </p>
403
+ </footer>
404
+ </body>
405
+ </html>