├── .gitignore
├── LICENSE
├── README.md
├── assets
├── 20241119_184227_image.png
├── 20241126_113713_image.png
└── 20241126_121117_example-page.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.js
├── Pages
│ ├── AccessDenied.js
│ ├── Debug.js
│ └── Information.js
├── components
│ ├── alert.js
│ ├── button.js
│ ├── deviceinfo.js
│ ├── grouplist.js
│ ├── history.js
│ ├── navbar.js
│ ├── originalurl.js
│ ├── pagetitle.js
│ ├── posture.js
│ ├── setup.js
│ ├── specialgroup.js
│ ├── status.css
│ └── warpinfo.js
├── content
│ └── AccessDeniedInfo.js
├── hooks
│ └── useSessionCheck.js
├── img
│ └── cf_favicon.png
├── index.css
├── index.js
├── main.js
├── package-lock.json
└── package.json
├── tailwind.config.js
└── wrangler.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /src/node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /.wrangler
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | node_modules
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cloudflare Identity and Access Help Page
2 |
3 | A highly customizable block page built in Cloudflare Workers that provides enriched Access Deny reasoning to end users. The page dynamically fetches user information from `/cdn-cgi/access/get-identity` and presents it in a user friendly format.
4 |
5 |
6 |
7 |
8 |
9 |
10 | Features of this page include:
11 |
12 | 1. Verifying and displaying if user has WARP enabled
13 | 2. Displaying device posture rule status for Crowdstrike
14 | 3. Dispalying device Operating System posture rules
15 | 4. Security Key usage
16 | 5. Correct Access team assignment
17 | 6. Displaying users device information
18 | 7. IDP group information and special IdP group identification
19 | 8. Recent Access login failures (last 3 in 30 minutes)
20 |
21 | ## Getting Started
22 |
23 | **Pre-Deployment Requirements**
24 |
25 | 1. Create an api token that has the following [permissions](https://developers.cloudflare.com/fundamentals/api/reference/permissions/), this will be used by the worker for data enrichment (apiv4 and graphQL requests).
26 |
27 | * Access: Audit Logs Read
28 | * Access: Device Posture Read
29 | 2. Create a new Cloudflare Access application (Self Hosted) https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/
30 |
31 | * Apply the restrictions to this new application that would typically apply to your organization (ie. Allow only specific email domains/authorized users) - This will restrict external, potentially unauthorized requests to the worker.
32 | * Set the Application session duration to the lowest available option.
33 | 3. Update Access applications block page to point to the deny worker's domain.
34 |
35 | * 
36 |
37 | **Deploying Worker:**
38 |
39 | 1. Update the wrangler.toml;
40 |
41 | * Name: your worker name
42 | * workers_dev: Ideally keep this false as it will be using a custom domain, protected by Cloudflare Access.
43 | * Routes, Pattern: Set this to your previously created Access Application domain.
44 | * BEARER_TOKEN: This is the previously generated API token. Ideally this token should not exist in plain text in the codebase. This is commented out in the toml and instead is added as a workers [secret](https://developers.cloudflare.com/workers/configuration/secrets/)
45 | * CORS_ORIGIN: This set this to your domain, appended with `/debug`
46 | * ACCOUNT_ID: Your target account ID, used in constructing api requests.
47 | * ORGANIZATION_ID: Your target account ID, used in verifying if users are indeed registed to the correct Cloudflare ZT organization.
48 | * TARGET_GROUP: If you have a specific group identity that could be considered restricted, or flagged, then set this to the group name. If a user has the target group assigned to their identity, a notification will appear.
49 | 2. Create KV, this will be used for setting theme elements, as well as the uploaded logo. `wrangler kv:namespace create IDENTITY_DYNAMIC_THEME_STORE`
50 | 3. Update the kv_namespaces id in the wrangler.toml to match the newly created `IDENTITY_DYNAMIC_THEME_STORE`ID.
51 |
52 | **Customizing Theme elements:**
53 |
54 | 1. Set DEBUG = "true" in the wrangler.toml and if needed, redeploy the worker - this will allow enable the debug page.
55 | 2. Visit your-domain/debug
56 | 3. Upload logo and select color primary and secondary colors
57 |
58 | * 
59 |
60 |
61 | ## Additional details
62 |
63 | The worker exposes 3 endpoints and makes seperate 2 API calls for device and posture information. These are endpoints used by the worker internally using subrquests.
64 |
65 | * `api/userdetails`: combined json output from get-identity, and device + posture information
66 | * `api/history`: [GraphQL](https://developers.cloudflare.com/analytics/graphql-api/tutorials/querying-access-login-events/) output for Access logs
67 | * `api/env`: Exposed worker environment variables for component interaction
68 |
69 | **DEPENDANCES**:
70 |
71 | * react-loader-spinner
72 | * react-router-dom
73 | * react-dom
74 | * tailwindcss
75 |
76 | **Page Summary:**
77 |
78 | * AccessDenied.js
79 | * Displays the main content of the worker at /access-denied.
80 | * Debug.js
81 | * Is used for intial setup and theme configuration, as well as debugging data from get-identity
82 | * Information.js
83 | * Can be configured to display any additional information or FAQs for end users
84 |
85 | **Component Summary:**
86 |
87 | * Overview.js
88 | * Contains warp information + Device information
89 | * Posture.js
90 | * Contains posture information, does the correlation checks for overal status
91 | * History.js
92 | * Interacts with the /api/history endpoint, displays Access login responses from graphql
93 | * Setup.js
94 | * Theme management and logo uploads
95 | * OriginalUrl
96 | * Shows the "redirected from" message when blocked.
97 | * Specialgroup.js
98 | * Used for risk-reduction group notification, this is defined in the wrangler.toml as "TARGET_GROUP"
99 |
--------------------------------------------------------------------------------
/assets/20241119_184227_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudflare/cf-identity-dynamic/6eaa2dec75a1b3e23eeecf6316daebc93520a4fa/assets/20241119_184227_image.png
--------------------------------------------------------------------------------
/assets/20241126_113713_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudflare/cf-identity-dynamic/6eaa2dec75a1b3e23eeecf6316daebc93520a4fa/assets/20241126_113713_image.png
--------------------------------------------------------------------------------
/assets/20241126_121117_example-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudflare/cf-identity-dynamic/6eaa2dec75a1b3e23eeecf6316daebc93520a4fa/assets/20241126_121117_example-page.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "react": "^18.2.0",
10 | "react-dom": "^18.2.0",
11 | "react-loader-spinner": "^6.1.6",
12 | "react-router-dom": "^6.22.3",
13 | "react-scripts": "5.0.1",
14 | "web-vitals": "^2.1.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "autoprefixer": "^10.4.19",
42 | "postcss": "^8.4.38",
43 | "tailwindcss": "^3.4.3"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudflare/cf-identity-dynamic/6eaa2dec75a1b3e23eeecf6316daebc93520a4fa/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Identity and Access Help Page
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3 | import AccessDenied from "./Pages/AccessDenied";
4 | import Information from "./Pages/Information";
5 | import Debug from "./Pages/Debug";
6 | import NavBar from "./components/navbar";
7 | import useSessionCheck from "./hooks/useSessionCheck";
8 | import PageTitle from "./components/pagetitle";
9 |
10 | const App = () => {
11 | const [debugEnabled, setDebugEnabled] = useState(false);
12 | const [setSessionExpired] = useState(false);
13 | const [theme, setTheme] = useState({
14 | primaryColor: "#3498db",
15 | secondaryColor: "#2ecc71",
16 | });
17 |
18 | // Fetch theme and debug status from the API
19 | useEffect(() => {
20 | const fetchEnv = async () => {
21 | try {
22 | const response = await fetch("/api/env");
23 | const data = await response.json();
24 | const themeData = data.theme || {};
25 | setTheme({
26 | primaryColor: themeData.primaryColor || "#3498db",
27 | secondaryColor: themeData.secondaryColor || "#2ecc71",
28 | });
29 |
30 | // Update debugEnabled based on the DEBUG value from the API
31 | setDebugEnabled(data.DEBUG === "true");
32 | } catch (error) {
33 | console.error("Error fetching environment variables:", error);
34 | }
35 | };
36 |
37 | fetchEnv();
38 | }, []);
39 |
40 | // This function will refresh the data when session expires
41 | const handleSessionExpired = () => {
42 | setSessionExpired(true);
43 | window.location.reload(); // Force page reload to refresh session
44 | };
45 |
46 | // Use the session check hook to detect session expiration
47 | useSessionCheck(handleSessionExpired);
48 |
49 | return (
50 |
51 |
56 |
57 |
58 |
65 | }
66 | />
67 |
74 | }
75 | />
76 | } />
77 | {debugEnabled && } />}
78 |
79 |
80 | );
81 | };
82 |
83 | export default App;
84 |
--------------------------------------------------------------------------------
/src/Pages/AccessDenied.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useLocation } from "react-router-dom";
3 | import Alert from "../components/alert";
4 | import Button from "../components/button";
5 | import OriginalUrl from "../components/originalurl";
6 | import Posture from "../components/posture";
7 | import History from "../components/history";
8 | import AccessDeniedInfo from "../content/AccessDeniedInfo";
9 | import DeviceInfo from "../components/deviceinfo";
10 | import WarpInfo from "../components/warpinfo";
11 | import { MutatingDots } from "react-loader-spinner";
12 |
13 | const AccessDenied = ({ primaryColor, secondaryColor }) => {
14 | const [loadingDeviceInfo, setLoadingDeviceInfo] = useState(true);
15 | const [loadingWarpInfo, setLoadingWarpInfo] = useState(true);
16 | const [loadingHistory, setLoadingHistory] = useState(true);
17 |
18 | const location = useLocation();
19 |
20 | // Handlers to update loading state
21 | const handleDeviceInfoLoaded = () => {
22 | setLoadingDeviceInfo(false);
23 | };
24 |
25 | const handleWarpInfoLoaded = () => {
26 | setLoadingWarpInfo(false);
27 | };
28 |
29 | const handleHistoryLoaded = () => {
30 | setLoadingHistory(false);
31 | };
32 |
33 | // Reset state when the route changes, this is needed to force refresh to keep the information up to date.
34 | useEffect(() => {
35 | setLoadingDeviceInfo(true);
36 | setLoadingWarpInfo(true);
37 | setLoadingHistory(true);
38 | }, [location.pathname]); // Reset loading states whenever the pathname changes
39 |
40 | const loadingPage = loadingDeviceInfo || loadingWarpInfo || loadingHistory;
41 |
42 | return (
43 |
44 | {/* Loading Overlay, this can be customized to your liking */}
45 | {loadingPage && (
46 |
47 |
56 |
57 | Checking your connection
58 |
59 |
60 | )}
61 |
62 | {/* Main Content */}
63 |
68 |
Access Denied
69 |
70 |
71 |
72 |
73 |
74 |
Overview
75 |
76 | One of the following sections describes why you were denied access to
77 | the site you attempted to visit. Currently, Cloudflare Access does not
78 | provide the exact reason for denial, therefore, this Access Denied
79 | page describes the most common reasons for access denials.
80 |
81 |
82 |
83 | Please review the provided information in order to troubleshoot any
84 | potential user, group, or device requirements that are not met.
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | {/* Set this up to be your intended "click to contact" support section (redirect or email).*/}
104 |
105 |
116 |
117 |
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default AccessDenied;
125 |
--------------------------------------------------------------------------------
/src/Pages/Debug.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback } from "react";
2 | import Setup from "../components/setup";
3 |
4 | const Debug = () => {
5 | const [debugInfo, setDebugInfo] = useState(null);
6 | const [devicePosture, setDevicePosture] = useState(null);
7 | const [error, setError] = useState(null);
8 | const [isIdentityExpanded, setIsIdentityExpanded] = useState(false);
9 | const [isDeviceExpanded, setIsDeviceExpanded] = useState(false);
10 |
11 | const forcePageReload = () => {
12 | window.location.reload();
13 | };
14 |
15 | const fetchDebugInfo = useCallback(async (retry = 1) => {
16 | try {
17 | const response = await fetch("/api/debug", { cache: "no-store" });
18 | const contentType = response.headers.get("Content-Type");
19 |
20 | if (contentType && contentType.includes("text/html")) {
21 | throw new Error("Session expired or invalid response");
22 | }
23 |
24 | const data = await response.json();
25 |
26 | setDebugInfo(data);
27 |
28 | if (data && data.devicePosture) {
29 | setDevicePosture(data.devicePosture);
30 | }
31 |
32 | setError(null);
33 | } catch (error) {
34 | console.error("Error fetching debug information:", error.message);
35 |
36 | // Retry logic for invalid session or response error
37 | if (retry > 0) {
38 | console.log("Retrying fetch for fresh session...");
39 | setTimeout(() => fetchDebugInfo(retry - 1), 1000);
40 | } else {
41 | setError("Session expired. Refreshing for a new session...");
42 | setTimeout(() => {
43 | forcePageReload();
44 | }, 1000);
45 | }
46 | }
47 | }, []);
48 |
49 | useEffect(() => {
50 | fetchDebugInfo();
51 | }, [fetchDebugInfo]);
52 |
53 | return (
54 |
55 |
56 |
57 | Debug Information
58 |
59 |
60 | {/* Collapsible get-identity */}
61 |
62 |
setIsIdentityExpanded(!isIdentityExpanded)}
65 | >
66 | {isIdentityExpanded ? "▼" : "►"} get-identity response
67 |
68 |
69 | {isIdentityExpanded &&
70 | (error ? (
71 |
{error}
72 | ) : (
73 |
74 | {JSON.stringify(debugInfo, null, 2)}
75 |
76 | ))}
77 |
78 |
79 | {/* Collapsible Device Posture */}
80 |
81 |
setIsDeviceExpanded(!isDeviceExpanded)}
84 | >
85 | {isDeviceExpanded ? "▼" : "►"} Device Posture Information
86 |
87 |
88 | {isDeviceExpanded &&
89 | (devicePosture ? (
90 |
91 |
Device Posture details:
92 |
93 | {Object.keys(devicePosture).map((key) => (
94 |
95 |
96 | {devicePosture[key].rule_name || "Unnamed Rule"} (
97 | {devicePosture[key].type})
98 |
99 |
100 | Success: {" "}
101 | {devicePosture[key].success ? "Yes" : "No"}
102 |
103 |
104 | Error: {" "}
105 | {devicePosture[key].error || "No errors"}
106 |
107 |
108 | Timestamp: {" "}
109 | {new Date(
110 | devicePosture[key].timestamp
111 | ).toLocaleString()}
112 |
113 |
114 | {JSON.stringify(devicePosture[key].input, null, 2)}
115 |
116 |
117 | ))}
118 |
119 |
120 | ) : (
121 |
122 | No device posture information available.
123 |
124 | ))}
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | );
133 | };
134 |
135 | export default Debug;
136 |
--------------------------------------------------------------------------------
/src/Pages/Information.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Information = () => {
4 | return (
5 |
6 |
7 |
Information
8 |
9 |
10 |
11 |
12 | {/* FAQs Section */}
13 |
14 |
Frequently Asked Questions
15 |
16 |
17 |
18 | How do I identify my current Gateway organization?
19 |
20 |
21 | If you need to confirm the currently enrolled Gateway organization
22 | for your WARP client instance, run the command{" "}
23 | warp-cli registration show
in your terminal
24 | (Linux/MacOS) or command prompt (Windows).
25 |
26 |
27 |
28 |
29 |
30 |
31 |
Example FAQ 1
32 |
Example answer and details.
33 |
34 |
35 |
36 |
37 |
38 |
Example FAQ 2
39 |
40 | https://developers.cloudflare.com/cloudflare-one/faq/troubleshooting/
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default Information;
50 |
--------------------------------------------------------------------------------
/src/components/alert.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Alert = ({ type, children }) => {
4 | const alertClass = () => {
5 | switch (type) {
6 | case 'success':
7 | return 'bg-alert-green text-alert-green2 border-alert-green p-4 rounded';
8 | case 'danger':
9 | return 'bg-alertred text-white border-red p-4 rounded';
10 | case 'warning':
11 | return 'bg-yellow text-warning border-warning p-4 rounded';
12 | default:
13 | return 'bg-gray text-gray-dark border-gray-light p-4 rounded';
14 | }
15 | };
16 |
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export default Alert;
25 |
--------------------------------------------------------------------------------
/src/components/button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Button = ({ children, variant = 'primary', className = '', onClick, secondaryColor, ...props }) => {
4 | const baseStyles =
5 | 'font-bold py-1 px-2.5 rounded focus:outline-none focus:shadow-outline transition-all duration-200 ease-in-out';
6 |
7 | const dynamicStyle = {
8 | backgroundColor: secondaryColor,
9 | color: 'white',
10 | transform: 'scale(1)', // allows it to shrink when clicked
11 | };
12 |
13 | return (
14 | (e.currentTarget.style.transform = 'scale(0.95)')} // Shrink on press
19 | onMouseUp={(e) => (e.currentTarget.style.transform = 'scale(1)')} // Reset on release
20 | onMouseEnter={(e) => {
21 | const currentColor = secondaryColor;
22 | e.currentTarget.style.backgroundColor = lightenColor(currentColor, 20); // highlight when hobered
23 | }}
24 | onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = secondaryColor)}
25 | {...props}
26 | >
27 | {children}
28 |
29 | );
30 | };
31 |
32 | // Utility function to lighten a color
33 | // https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
34 | const lightenColor = (color, percent) => {
35 | const num = parseInt(color.slice(1), 16),
36 | amt = Math.round(2.55 * percent),
37 | R = (num >> 16) + amt,
38 | G = ((num >> 8) & 0x00ff) + amt,
39 | B = (num & 0x0000ff) + amt;
40 |
41 | return `#${(
42 | 0x1000000 +
43 | (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
44 | (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
45 | (B < 255 ? (B < 1 ? 0 : B) : 255)
46 | )
47 | .toString(16)
48 | .slice(1)}`;
49 | };
50 |
51 | export default Button;
52 |
--------------------------------------------------------------------------------
/src/components/deviceinfo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import "./status.css";
3 |
4 | const DeviceInfo = ({ onLoaded }) => {
5 | const [userData, setUserData] = useState({
6 | device_model: "",
7 | device_name: "",
8 | device_os_ver: "",
9 | device_ID: "",
10 | is_WARP_enabled: false,
11 | });
12 | const [warpEnabled, setWarpEnabled] = useState(false);
13 | const [errorMessage, setErrorMessage] = useState("");
14 |
15 | useEffect(() => {
16 | const fetchWarpStatus = async () => {
17 | try {
18 | console.log("DeviceInfo: Fetching WARP status...");
19 | const traceResponse = await fetch("https://www.cloudflare.com/cdn-cgi/trace");
20 | const traceText = await traceResponse.text();
21 | const warpStatus = traceText.includes("warp=on");
22 | setWarpEnabled(warpStatus);
23 |
24 | if (warpStatus) {
25 | console.log("DeviceInfo: WARP is enabled, fetching user data...");
26 | fetchUserData(); //fetch user data if WARP is enabled
27 | } else {
28 | // setErrorMessage("WARP is not enabled. Device information is unavailable.");
29 | if (onLoaded) onLoaded();
30 | }
31 | } catch (error) {
32 | console.error("DeviceInfo: Error fetching WARP status:", error);
33 | setErrorMessage("Error fetching WARP status. Please try again later.");
34 | if (onLoaded) onLoaded(); // send loading status back to accessdenied.js
35 | }
36 | };
37 |
38 | const fetchUserData = async () => {
39 | try {
40 | const response = await fetch("/api/userdetails");
41 | const data = await response.json();
42 |
43 | setUserData({
44 | device_model: data.device?.result?.model || "",
45 | device_name: data.device?.result?.name || "",
46 | device_os_ver: data.device?.result?.os_version || "",
47 | device_ID: data.device?.result?.gateway_device_id || "",
48 | is_WARP_enabled: true,
49 | });
50 |
51 | console.log("DeviceInfo: User data loaded successfully.");
52 | if (onLoaded) onLoaded(); // send loading status back to accessdenied.js
53 | } catch (error) {
54 | console.error("DeviceInfo: Error fetching device data:", error);
55 | // setErrorMessage("Error fetching device data. Please refresh the page or try again later.");
56 | if (onLoaded) onLoaded(); // send loading status back to accessdenied.js still
57 | }
58 | };
59 |
60 | fetchWarpStatus();
61 | }, [onLoaded]);
62 |
63 | return (
64 |
65 | {warpEnabled ? (
66 | userData.is_WARP_enabled ? (
67 | <>
68 |
Device Information
69 |
70 |
71 |
72 | Device Model:
73 | {userData.device_model}
74 |
75 |
76 |
77 | Device Name:
78 | {userData.device_name}
79 |
80 |
81 |
82 | OS Version:
83 | {userData.device_os_ver}
84 |
85 |
86 |
87 | Serial Number:
88 | {userData.device_ID}
89 |
90 |
91 | >
92 | ) : (
93 |
94 |
95 | Device information unavailable.
96 |
97 | )
98 | ) : (
99 |
100 |
101 | {errorMessage || "WARP is not enabled. Please enable WARP to view device information."}
102 |
103 | )}
104 | {errorMessage &&
{errorMessage}
}
105 |
106 | );
107 | };
108 |
109 | export default DeviceInfo;
110 |
--------------------------------------------------------------------------------
/src/components/grouplist.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { MutatingDots } from 'react-loader-spinner';
3 |
4 | const GroupList = () => {
5 | const [groups, setGroups] = useState([]);
6 | const [searchTerm, setSearchTerm] = useState('');
7 | const [expanded, setExpanded] = useState(false);
8 | const [loading, setLoading] = useState(true); // Loading state
9 | const [error, setError] = useState(null); // Error state
10 | const defaultVisibleGroups = 5; // this can be changed, for now its 5
11 |
12 | const filteredGroups = groups
13 | .filter(group =>
14 | group.toLowerCase().includes(searchTerm.toLowerCase())
15 | )
16 | .sort((a, b) => a.localeCompare(b)); // Sort alphabetically
17 |
18 | // expand the group list
19 | const toggleExpand = () => {
20 | setExpanded(!expanded);
21 | };
22 |
23 | // Fetch groups from the get-identity API
24 | useEffect(() => {
25 | const fetchGroupData = async () => {
26 | try {
27 | const response = await fetch('/api/userdetails');
28 | const data = await response.json();
29 |
30 | if (data && data.identity && data.identity.groups) {
31 | setGroups(data.identity.groups);
32 | } else {
33 | setError('No group data available');
34 | }
35 | } catch (err) {
36 | console.error('Error fetching group data:', err);
37 | setError('Error fetching group data');
38 | } finally {
39 | setLoading(false);
40 | }
41 | };
42 |
43 | fetchGroupData();
44 | }, []);
45 |
46 | return (
47 |
48 |
49 |
Your Current Groups
50 | setSearchTerm(e.target.value)}
56 | value={searchTerm}
57 | />
58 |
59 |
60 | {loading ? (
61 |
62 |
63 |
64 | ) : error ? (
65 |
{error}
66 | ) : (
67 | <>
68 |
69 | {filteredGroups.slice(0, expanded ? filteredGroups.length : defaultVisibleGroups).map((group, index) => (
70 | {group}
71 | ))}
72 |
73 |
74 |
82 | {expanded ? 'Collapse list' : 'Expand list'}
83 |
84 | >
85 | )}
86 |
87 | );
88 | };
89 |
90 | export default GroupList;
91 |
--------------------------------------------------------------------------------
/src/components/history.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import "./status.css";
3 |
4 | const History = ({ onLoaded }) => {
5 | const [loginHistory, setLoginHistory] = useState([]);
6 | const [warpEnabled, setWarpEnabled] = useState(null);
7 | const [errorMessage, setErrorMessage] = useState("");
8 |
9 | useEffect(() => {
10 | const fetchData = async () => {
11 | try {
12 | console.log("History: Fetching WARP status...");
13 |
14 | // Fetch WARP status from trace
15 | const traceResponse = await fetch("https://www.cloudflare.com/cdn-cgi/trace");
16 | const traceText = await traceResponse.text();
17 | const warpStatus = traceText.includes("warp=on");
18 | setWarpEnabled(warpStatus);
19 |
20 | if (!warpStatus) {
21 | console.warn("History: WARP is not enabled.");
22 | setLoginHistory(null);
23 | if (onLoaded) onLoaded(); // send loading status back to accessdenied.js
24 | return;
25 | }
26 |
27 | console.log("History: Fetching login history...");
28 | // Fetch login history
29 | const response = await fetch("/api/history");
30 | if (!response.ok) {
31 | if (response.status >= 400 && response.status < 500) {
32 | setLoginHistory(null); // No login history available
33 | } else {
34 | throw new Error("Failed to fetch login history");
35 | }
36 | } else {
37 | const data = await response.json();
38 | const historyEntries = data?.loginHistory || [];
39 |
40 | if (historyEntries.length === 0) {
41 | console.warn("History: No login failures found.");
42 | setLoginHistory(null); // No login failures
43 | } else {
44 | const historyData = historyEntries.slice(-3).map((entry) => ({
45 | date: new Date(entry.dimensions.datetime).toLocaleDateString(),
46 | time: new Date(entry.dimensions.datetime).toLocaleTimeString(),
47 | applicationName: entry.applicationName || "Unknown App",
48 | reason: getReason(entry.dimensions),
49 | }));
50 | setLoginHistory(historyData);
51 | }
52 | }
53 |
54 | console.log("History: Data fetch complete.");
55 | } catch (error) {
56 | console.error("History: Error fetching data:", error);
57 | setErrorMessage("Error fetching login history. Please try again later.");
58 | } finally {
59 | if (onLoaded) onLoaded();
60 | }
61 | };
62 |
63 | const getReason = ({ hasGatewayEnabled, hasWarpEnabled }) => {
64 | if (hasGatewayEnabled === 0) {
65 | return { label: "Gateway", color: "bg-red" };
66 | }
67 | if (hasWarpEnabled === 0) {
68 | return { label: "WARP", color: "bg-red" };
69 | }
70 | return { label: "Other", color: "bg-red" };
71 | };
72 |
73 | fetchData();
74 | }, [onLoaded]);
75 |
76 | return (
77 |
78 | {warpEnabled ? (
79 | <>
80 |
Recent Access Login Failures
81 | {loginHistory ? (
82 |
83 |
84 |
85 |
86 | Date
87 | Time
88 |
89 | Application Name
90 |
91 | Reason
92 |
93 |
94 |
95 | {loginHistory.map((entry, index) => (
96 |
97 | {entry.date}
98 | {entry.time}
99 | {entry.applicationName}
100 |
101 |
104 | {entry.reason.label}
105 |
106 |
107 |
108 | ))}
109 |
110 |
111 |
112 | ) : (
113 |
No recent Access login failures observed.
114 | )}
115 | >
116 | ) : (
117 |
118 |
119 | {errorMessage || "Please enable WARP to view detailed login history."}
120 |
121 | )}
122 | {errorMessage &&
{errorMessage}
}
123 |
124 | );
125 | };
126 |
127 | export default History;
128 |
--------------------------------------------------------------------------------
/src/components/navbar.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | const NavBar = ({ debugEnabled, primaryColor }) => {
5 | const [logoUrl, setLogoUrl] = useState(null);
6 |
7 | useEffect(() => {
8 | const fetchLogo = async () => {
9 | try {
10 | const response = await fetch('/assets/logo'); // use the logo uploaded in debug - QoL
11 | if (!response.ok) {
12 | throw new Error('Failed to fetch logo from KV');
13 | }
14 | const blob = await response.blob();
15 | const logoUrl = URL.createObjectURL(blob);
16 | setLogoUrl(logoUrl);
17 | } catch (error) {
18 | console.error('Error fetching logo from KV:', error);
19 | }
20 | };
21 |
22 | fetchLogo();
23 | }, []);
24 |
25 | const getNavLinkClass = ({ isActive }) =>
26 | isActive
27 | ? 'bg-steel text-black px-4 h-full flex items-center rounded-t-md'
28 | : 'text-white no-underline px-4 h-full flex items-center hover:bg-white hover:bg-opacity-20 rounded-t-md';
29 |
30 | return (
31 |
35 |
36 |
37 | {logoUrl ? (
38 |
43 | ) : (
44 | Loading Logo...
45 | )}
46 |
47 |
Identity and Access Help Page
48 |
49 |
50 |
51 |
52 | Access Denied
53 |
54 |
55 |
56 |
57 | Information
58 |
59 |
60 | {debugEnabled && (
61 |
62 |
63 | Debug
64 |
65 |
66 | )}
67 |
68 |
69 | );
70 | };
71 |
72 | export default NavBar;
73 |
--------------------------------------------------------------------------------
/src/components/originalurl.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | const OriginalUrl = () => {
4 | const [originalUrl, setOriginalUrl] = useState('');
5 | const [isUrlValid, setIsUrlValid] = useState(true);
6 |
7 | useEffect(() => {
8 | // Extract the original_url parameter from the URL query string
9 | const urlParams = new URLSearchParams(window.location.search);
10 | const original_url = urlParams.get('original_url');
11 |
12 | if (original_url) {
13 | try {
14 | let url = new URL(original_url);
15 | setOriginalUrl(url.href);
16 | } catch (error) {
17 | // If there is an error parsing the URL, assume it's a relative URL or a typo and prepend "https://"
18 | setOriginalUrl(`https://${original_url}`);
19 | }
20 | } else {
21 | setIsUrlValid(false);
22 | }
23 | }, []);
24 |
25 | const handleButtonClick = () => {
26 | window.location.href = originalUrl;
27 | };
28 |
29 | if (!isUrlValid) {
30 | return null;
31 | }
32 |
33 | return (
34 |
35 |
36 | Refresh Access Application
37 |
38 |
39 | );
40 | };
41 |
42 | export default OriginalUrl;
43 |
--------------------------------------------------------------------------------
/src/components/pagetitle.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | // This just changes the browser tab title
5 | // sloppy implementation, but easily customizable
6 | const PageTitle = () => {
7 | const location = useLocation();
8 |
9 | useEffect(() => {
10 | switch (location.pathname) {
11 | case "/access-denied":
12 | document.title = "Access Denied";
13 | break;
14 | case "/information":
15 | document.title = "Information";
16 | break;
17 | case "/debug":
18 | document.title = "Debug";
19 | break;
20 | default:
21 | document.title = "Identity and Access Help Page";
22 | }
23 | }, [location]);
24 |
25 | return null;
26 | };
27 |
28 | export default PageTitle;
29 |
--------------------------------------------------------------------------------
/src/components/posture.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { createPortal } from "react-dom";
3 | import "./status.css";
4 |
5 | const Posture = ({ onLoaded }) => {
6 | const [osPostureChecks, setOsPostureChecks] = useState([]);
7 | const [securityKey, setSecurityKey] = useState(null);
8 | const [crowdstrikeStatus, setCrowdstrikeStatus] = useState(null);
9 | const [osStatus, setOsStatus] = useState({ message: "", passed: false });
10 | const [warpEnabled, setWarpEnabled] = useState(null);
11 | const [tooltipStyles, setTooltipStyles] = useState({});
12 | const [errorMessage, setErrorMessage] = useState("");
13 |
14 | const tooltipTriggerRef = useRef(null);
15 |
16 | useEffect(() => {
17 | const fetchData = async () => {
18 | try {
19 | console.log("Posture: Fetching data...");
20 |
21 | // Fetch WARP status
22 | const traceResponse = await fetch(
23 | "https://www.cloudflare.com/cdn-cgi/trace"
24 | );
25 | const traceText = await traceResponse.text();
26 | const warpStatus = traceText.includes("warp=on");
27 | setWarpEnabled(warpStatus);
28 |
29 | if (!warpStatus) {
30 | setSecurityKey(null);
31 | setCrowdstrikeStatus(null);
32 | setOsStatus({
33 | message: "Posture information unavailable, please enable WARP",
34 | passed: false,
35 | });
36 | if (onLoaded) onLoaded(); // Notify parent that loading is complete
37 | return;
38 | }
39 |
40 | // Fetch user details
41 | const response = await fetch("/api/userdetails");
42 | const data = await response.json();
43 |
44 | /*
45 | Note that this looks strictly for the presence/use of "swk" (Proof of possession of a software-secured key)
46 | */
47 |
48 | const securityKeyInUse = data.identity?.amr?.includes("swk") || false;
49 | setSecurityKey(
50 | securityKeyInUse
51 | ? "Security Key in Use"
52 | : "Security Key is not in Use"
53 | );
54 |
55 | /*
56 | This may not be applicable to your environment, requires that Crowdstrike as a posture source be added and rules configured.
57 | */
58 |
59 | // CrowdStrike Check
60 | const postureRules = data.posture?.result || {};
61 | let crowdstrikePassed = false;
62 |
63 | for (const rule of Object.values(postureRules)) {
64 | if (rule.type === "crowdstrike_s2s" && rule.success) {
65 | crowdstrikePassed = true;
66 | break;
67 | }
68 | }
69 |
70 | setCrowdstrikeStatus(
71 | crowdstrikePassed
72 | ? "CrowdStrike posture check successful"
73 | : "CrowdStrike posture check failed"
74 | );
75 |
76 | /*
77 | This also requires that rules exist for min-max values for Operating system versions
78 | https://developers.cloudflare.com/cloudflare-one/identity/devices/warp-client-checks/os-version/#enable-the-os-version-check
79 | */
80 |
81 | // OS Posture Check
82 | let relevantRules = [];
83 | let allConstraintsPassed = true;
84 |
85 | for (const rule of Object.values(postureRules)) {
86 | if (rule.type === "os_version") {
87 | relevantRules.push({
88 | name: rule.rule_name,
89 | success: rule.success,
90 | description: rule.description || "No description available",
91 | checked: rule.hasOwnProperty("check"),
92 | isMinConstraint: rule.rule_name
93 | .toLowerCase()
94 | .includes("min constraint"),
95 | isPatch: rule.rule_name.toLowerCase().includes("patch"),
96 | });
97 |
98 | if ((rule.isMinConstraint || rule.isPatch) && !rule.success) {
99 | allConstraintsPassed = false;
100 | }
101 | }
102 | }
103 |
104 | // Empty OS checks - For niece devices like chromeOS
105 | if (relevantRules.length === 0) {
106 | relevantRules.push({
107 | name: "OS Version Check",
108 | success: false,
109 | description:
110 | "No relevant OS version rules found for this device type",
111 | checked: false,
112 | });
113 | allConstraintsPassed = false;
114 | }
115 |
116 | setOsPostureChecks(relevantRules);
117 |
118 | // Set OS Status Message
119 | setOsStatus({
120 | message: allConstraintsPassed
121 | ? "Operating system up to date"
122 | : "Operating system update required",
123 | passed: allConstraintsPassed,
124 | });
125 |
126 | console.log("Posture: Data fetch complete.");
127 | } catch (error) {
128 | console.error("Posture: Error fetching data:", error);
129 | // setErrorMessage("Error fetching posture data. Please try again later.");
130 | } finally {
131 | if (onLoaded) onLoaded(); // Notify parent that loading is complete
132 | }
133 | };
134 |
135 | fetchData();
136 | }, [onLoaded]);
137 |
138 | // Allow hover-over of the OS results to see detailed information, useful for debugging the rules
139 | const handleMouseEnter = () => {
140 | if (tooltipTriggerRef.current) {
141 | const rect = tooltipTriggerRef.current.getBoundingClientRect();
142 | setTooltipStyles({
143 | top: rect.bottom + window.scrollY + 5,
144 | left: rect.left + window.scrollX,
145 | });
146 | }
147 | };
148 |
149 | const handleMouseLeave = () => {
150 | setTooltipStyles({});
151 | };
152 |
153 | const allPassed =
154 | warpEnabled &&
155 | securityKey === "Security Key in Use" &&
156 | crowdstrikeStatus?.includes("successful") &&
157 | osStatus.passed;
158 |
159 | return (
160 |
161 | {warpEnabled ? (
162 | <>
163 |
164 | Device Posture Requirements
165 |
166 |
167 | {/* Security Key Status */}
168 |
169 |
176 | {securityKey}
177 |
178 |
179 | {/* CrowdStrike Status */}
180 |
181 |
188 | {crowdstrikeStatus}
189 |
190 |
191 | {/* OS status */}
192 |
198 |
203 | {osStatus.message}
204 | {tooltipStyles.top &&
205 | createPortal(
206 |
214 |
215 | {osPostureChecks.map((check, index) => (
216 |
217 |
222 | {`${check.name}: ${
223 | check.checked
224 | ? check.success
225 | ? "Compliant"
226 | : "Non-compliant"
227 | : "Rule was not checked"
228 | }`}
229 |
230 | ))}
231 |
232 |
,
233 | document.body
234 | )}
235 |
236 |
237 | >
238 | ) : (
239 |
240 |
241 | Please enable WARP to view device posture information.
242 |
243 | )}
244 | {errorMessage &&
{errorMessage}
}
245 |
246 | );
247 | };
248 |
249 | export default Posture;
250 |
--------------------------------------------------------------------------------
/src/components/setup.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | const Setup = () => {
4 | const [primaryColor, setPrimaryColor] = useState("#3498db");
5 | const [secondaryColor, setSecondaryColor] = useState("#2ecc71");
6 | const [statusMessage, setStatusMessage] = useState("");
7 | const [isSaving, setIsSaving] = useState(false);
8 | const [tooltip, setTooltip] = useState({});
9 | const [logo, setLogo] = useState(null);
10 | const [logoPreview, setLogoPreview] = useState(null);
11 |
12 | useEffect(() => {
13 | const fetchTheme = async () => {
14 | try {
15 | const response = await fetch("/api/env");
16 | if (!response.ok) {
17 | throw new Error("Failed to fetch theme configuration");
18 | }
19 | const data = await response.json();
20 | const theme = data.theme || {};
21 |
22 | setPrimaryColor(theme.primaryColor || "#3498db");
23 | setSecondaryColor(theme.secondaryColor || "#2ecc71");
24 | setLogo(theme.logoUrl || null);
25 |
26 | if (theme.logoUrl) setLogoPreview(`/assets/logo`);
27 |
28 | applyTheme(theme.primaryColor, theme.secondaryColor);
29 | } catch (error) {
30 | console.error("Error fetching theme:", error);
31 | }
32 | };
33 |
34 | fetchTheme();
35 | // eslint-disable-next-line
36 | }, []);
37 |
38 | const applyTheme = (primary, secondary) => {
39 | document.documentElement.style.setProperty(
40 | "--primary-color",
41 | primary || primaryColor
42 | );
43 | document.documentElement.style.setProperty(
44 | "--secondary-color",
45 | secondary || secondaryColor
46 | );
47 | };
48 |
49 | const handleSaveTheme = async () => {
50 | const theme = { primaryColor, secondaryColor };
51 | setIsSaving(true);
52 | setStatusMessage("");
53 |
54 | try {
55 | // Save theme colors
56 | const response = await fetch("/api/env", {
57 | method: "POST",
58 | headers: { "Content-Type": "application/json" },
59 | body: JSON.stringify(theme),
60 | });
61 |
62 | if (!response.ok) {
63 | throw new Error("Failed to save theme configuration");
64 | }
65 |
66 | // Upload logo
67 | if (logo) await uploadImage(logo, "logo");
68 |
69 | setStatusMessage("Saved successfully!");
70 | } catch (error) {
71 | console.error("Error saving theme:", error);
72 | setStatusMessage("Error saving changes. Please refresh and try again.");
73 | } finally {
74 | setIsSaving(false);
75 | setTimeout(() => setStatusMessage(""), 5000); // Clear message after 5 seconds
76 | }
77 | };
78 |
79 | const uploadImage = async (file, type) => {
80 | const formData = new FormData();
81 | formData.append("file", file);
82 | formData.append("type", type);
83 |
84 | const response = await fetch("/api/upload", {
85 | method: "POST",
86 | body: formData,
87 | });
88 |
89 | if (!response.ok) {
90 | throw new Error(`Failed to upload ${type}`);
91 | }
92 | };
93 |
94 | const handleImageUpload = (event, setImage, setPreview) => {
95 | const file = event.target.files[0];
96 | if (
97 | file &&
98 | ["image/png", "image/jpeg", "image/svg+xml"].includes(file.type)
99 | ) {
100 | setImage(file); // Store the file
101 | const reader = new FileReader();
102 | reader.onload = () => setPreview(reader.result); // show preview of the file
103 | reader.readAsDataURL(file);
104 | } else {
105 | alert("Only PNG, JPG, and SVG files are allowed.");
106 | }
107 | };
108 |
109 | const renderColorPicker = (
110 | label,
111 | color,
112 | setColor,
113 | description,
114 | tooltipKey
115 | ) => (
116 |
117 |
118 |
{label}
119 |
setTooltip({ [tooltipKey]: description })}
122 | onMouseLeave={() => setTooltip({})}
123 | >
124 | {/* This is just to show one icon lol */}
125 |
133 |
134 |
135 | {tooltip[tooltipKey] && (
136 |
137 | {tooltip[tooltipKey]}
138 |
139 | )}
140 |
141 |
142 |
143 | setColor(e.target.value)}
147 | className="w-12 h-12 rounded"
148 | style={{ border: "none" }}
149 | />
150 | setColor(e.target.value)}
154 | className="w-24 border rounded mt-2 text-center"
155 | placeholder="#HEX"
156 | />
157 |
158 |
159 | );
160 |
161 | return (
162 |
163 |
Customize Theme
164 |
165 | {/* Image Upload Section */}
166 |
167 |
168 |
Upload Logo
169 |
173 | {logoPreview ? (
174 |
179 | ) : (
180 |
181 | Drag & Drop or Click to Upload
182 |
183 | )}
184 |
handleImageUpload(e, setLogo, setLogoPreview)}
189 | />
190 |
191 |
192 |
193 |
194 | {/* Color Pickers */}
195 |
196 | {renderColorPicker(
197 | "Primary",
198 | primaryColor,
199 | setPrimaryColor,
200 | "This is the color used by loading elements as well as the navigation bar.",
201 | "primaryColorTooltip"
202 | )}
203 | {renderColorPicker(
204 | "Secondary",
205 | secondaryColor,
206 | setSecondaryColor,
207 | "Used for link highlighting as well as buttons.",
208 | "secondaryColorTooltip"
209 | )}
210 |
211 |
212 | {/* Save Button */}
213 |
214 |
221 | {isSaving ? "Saving..." : "Save Theme"}
222 |
223 |
224 |
225 | {/* Status Message */}
226 | {statusMessage && (
227 |
{statusMessage}
228 | )}
229 |
230 | );
231 | };
232 |
233 | export default Setup;
234 |
--------------------------------------------------------------------------------
/src/components/specialgroup.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Alert from './alert';
3 |
4 | const SpecialGroup = () => {
5 | const [setTargetGroup] = useState('');
6 | const [isInTargetGroup, setIsInTargetGroup] = useState(false);
7 | const [loading, setLoading] = useState(true);
8 |
9 | useEffect(() => {
10 | // Fetch the environment variables to get the TARGET_GROUP (this is defined in the wrangler.toml/worker env vars)
11 | const fetchEnvVars = async () => {
12 | try {
13 | const response = await fetch('/api/env');
14 | const envData = await response.json();
15 | setTargetGroup(envData.TARGET_GROUP);
16 |
17 | const userDetailsResponse = await fetch('/api/userdetails');
18 | const userDetails = await userDetailsResponse.json();
19 |
20 | const userGroups = userDetails.identity?.groups || [];
21 | setIsInTargetGroup(userGroups.includes(envData.TARGET_GROUP));
22 |
23 | setLoading(false);
24 | } catch (error) {
25 | console.error('Error fetching environment variables or user details:', error);
26 | setLoading(false);
27 | }
28 | };
29 |
30 | fetchEnvVars();
31 | // eslint-disable-next-line
32 | }, []);
33 |
34 | if (loading) {
35 | return Loading group information...
;
36 | }
37 |
38 | // This can be changed to meet your own requirements, by default it shows messaging for a "example group"
39 | return (
40 |
41 | {isInTargetGroup ? (
42 |
43 |
44 |
45 | User is in an example group, please visit example.com if you believe this is an error.
46 |
47 |
48 |
49 | ) : (
50 |
51 |
52 | User is not in an example group.
53 |
54 |
55 | )}
56 |
57 | );
58 | };
59 |
60 | export default SpecialGroup;
61 |
--------------------------------------------------------------------------------
/src/components/status.css:
--------------------------------------------------------------------------------
1 | /* Styling for the icons */
2 | .icon {
3 | display: inline-block;
4 | width: 24px;
5 | height: 24px;
6 | margin-right: 8px;
7 | vertical-align: middle;
8 | }
9 |
10 | .info-icon {
11 | background-color: grey;
12 | mask: url('data:image/svg+xml;utf8, ') no-repeat center / contain;
13 | }
14 |
15 | .check-icon {
16 | background-color: green;
17 | mask: url('data:image/svg+xml;utf8, ') no-repeat center / contain;
18 | }
19 |
20 | .cross-icon {
21 | background-color: red;
22 | mask: url('data:image/svg+xml;utf8, ') no-repeat center / contain;
23 | }
24 |
25 | /* Tooltip (used in posture hover over) */
26 | .tooltip {
27 | position: absolute;
28 | top: 100%;
29 | left: 0;
30 | margin-top: 5px;
31 | background-color: white;
32 | padding: 8px;
33 | border-radius: 4px;
34 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
35 | z-index: 9999;
36 | pointer-events: none;
37 | }
38 |
39 | /* Used for the Access failures table */
40 | .truncate-text {
41 | max-width: 800px;
42 | overflow: hidden;
43 | white-space: nowrap;
44 | text-overflow: ellipsis;
45 | display: inline-block;
46 | vertical-align: middle;
47 | }
48 |
49 | /* AccessDenied page component layout */
50 | .flex-container {
51 | display: flex;
52 | gap: 4rem;
53 | }
54 |
55 | .flex-item {
56 | flex-grow: 1;
57 | min-height: 200px;
58 | display: flex;
59 | flex-direction: column;
60 | align-items: stretch;
61 | }
62 |
63 | .card-loading {
64 | background-color: transparent;
65 | height: 100%;
66 | padding: 0;
67 | border: none;
68 | }
69 |
70 | .card-normal {
71 | background-color: #ffffff;
72 | border: 1px solid #d1d5db;
73 | min-height: 250px;
74 | max-height: 250px;
75 | overflow-y: auto;
76 | padding: 20px;
77 | border-radius: 8px;
78 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
79 | }
80 |
81 | .card-error {
82 | background-color: #f6eeee;
83 | border: 1px solid #ff4848;
84 | height: 100%;
85 | padding: 20px;
86 | border-radius: 8px;
87 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
88 | }
89 |
90 | :root {
91 | --loader-color: #3498db;
92 | --navigation-color: #2ecc71;
93 | --accent-color: #e74c3c;
94 | }
95 |
96 | /* Loader styling using theme color */
97 | .loader {
98 | color: var(--loader-color);
99 | }
100 |
101 | .navbar {
102 | background-color: var(--navigation-color);
103 | }
104 |
105 | .accent {
106 | color: var(--accent-color);
107 | border-color: var(--accent-color);
108 | }
109 |
110 | .button:hover {
111 | background-color: var(--accent-color);
112 | color: white;
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/warpinfo.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | const WarpInfo = ({ onLoaded }) => {
4 | const [userData, setUserData] = useState({
5 | user_name: "",
6 | user_email: "",
7 | is_WARP_enabled: false,
8 | gateway_account_id: "",
9 | is_in_org: null,
10 | });
11 | const [envVars, setEnvVars] = useState({
12 | ORGANIZATION_ID: "",
13 | ORGANIZATION_NAME: "",
14 | });
15 | const [warpEnabled, setWarpEnabled] = useState(null);
16 | const [errorMessage] = useState("");
17 |
18 | useEffect(() => {
19 | const fetchData = async () => {
20 | try {
21 | console.log("WarpInfo: Starting data fetch...");
22 |
23 | // Fetch WARP status
24 | const traceResponse = await fetch("https://www.cloudflare.com/cdn-cgi/trace");
25 | const traceText = await traceResponse.text();
26 | const warpStatus = traceText.includes("warp=on");
27 | setWarpEnabled(warpStatus);
28 |
29 | // Fetch environment variables
30 | const envResponse = await fetch("/api/env");
31 | const envData = await envResponse.json();
32 | setEnvVars({
33 | ORGANIZATION_ID: envData.ORGANIZATION_ID,
34 | ORGANIZATION_NAME: envData.ORGANIZATION_NAME,
35 | });
36 |
37 | // Fetch user data
38 | const userResponse = await fetch("/api/userdetails");
39 | const userData = await userResponse.json();
40 |
41 | // Calculate is_in_org
42 | const isInOrg = userData.identity.gateway_account_id === envData.ORGANIZATION_ID;
43 |
44 | // Update userData state with fetched data and calculated `is_in_org`
45 | setUserData({
46 | user_name: userData.identity.name,
47 | user_email: userData.identity.email,
48 | is_WARP_enabled: userData.identity.is_warp,
49 | gateway_account_id: userData.identity.gateway_account_id,
50 | is_in_org: isInOrg,
51 | });
52 |
53 | console.log("WarpInfo: Data fetch complete, is_in_org:", isInOrg);
54 | } catch (error) {
55 | // console.error("WarpInfo: Error fetching data:", error);
56 | // setErrorMessage("Error fetching WARP or user data. Please try again later.");
57 | } finally {
58 | if (onLoaded) onLoaded(); // send loading status back to accessdenied.js
59 | }
60 | };
61 |
62 | fetchData();
63 | }, [onLoaded]);
64 |
65 | return (
66 |
67 | {warpEnabled ? (
68 | <>
69 |
WARP Information
70 |
71 |
72 |
73 | {userData.user_name}
74 |
75 |
76 |
77 | {userData.user_email}
78 |
79 |
80 | WARP is enabled
81 |
82 |
83 |
88 | {userData.is_in_org
89 | ? `User is in the "${envVars.ORGANIZATION_NAME}" organization`
90 | : `User is not in the "${envVars.ORGANIZATION_NAME}" organization`}
91 |
92 |
93 | >
94 | ) : (
95 |
96 |
97 | Please enable WARP to view detailed user information.
98 |
99 | )}
100 | {errorMessage &&
{errorMessage}
}
101 |
102 | );
103 | };
104 |
105 | export default WarpInfo;
106 |
--------------------------------------------------------------------------------
/src/content/AccessDeniedInfo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Alert from "../components/alert";
3 | import SpecialGroup from "../components/specialgroup";
4 | import GroupList from "../components/grouplist";
5 |
6 | const AccessDeniedInfo = ({ secondaryColor }) => {
7 | // const linkStyles = `underline text-[${secondaryColor}]`;
8 |
9 | /*
10 | This can be tailored to whatever contnet serves your needs, ideally this would provide a brief highlight of the information presented on the page (WARP/GW/Posture requirements)
11 | */
12 | return (
13 |
14 |
WARP Zero Trust Required
15 |
16 | WARP Zero Trust is required to be configured in Gateway with WARP mode to access this resource. The site you have
17 | attempted to access likely requires that WARP is currently enabled and protecting your session.
18 |
19 |
20 |
21 |
22 | Please ensure that your WARP Zero Trust client is Connected and reports that Your Internet is protected .
23 |
24 |
25 |
26 |
27 |
28 |
29 |
Device Posture
30 |
31 | In order to access this resource, your device must meet set security requirements. Please ensure that your device{" "}
32 | operating system is up to date and that all software has the latest patches and updates applied, where
33 | available.
34 |
35 |
36 |
37 |
38 | If you have failed the Crowdstrike Posture check, please reboot your device and try again.
39 |
40 |
41 |
42 |
43 |
44 |
45 |
Correct Group Membership Required
46 |
47 | The resource you attempted to visit requires membership in an authorized IDP group. Please review your current groups for more information.
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default AccessDeniedInfo;
62 |
--------------------------------------------------------------------------------
/src/hooks/useSessionCheck.js:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 |
3 | const useSessionCheck = (onSessionExpired) => {
4 | useEffect(() => {
5 | const checkSession = () => {
6 | const authCookie = document.cookie.split('; ').find(row => row.startsWith('CF_Authorization='));
7 | if (!authCookie && typeof onSessionExpired === 'function') {
8 | onSessionExpired();
9 | }
10 | };
11 |
12 | const intervalId = setInterval(checkSession, 60000); // Check every 60s
13 |
14 | return () => clearInterval(intervalId);
15 | }, [onSessionExpired]);
16 | };
17 |
18 | export default useSessionCheck;
19 |
--------------------------------------------------------------------------------
/src/img/cf_favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudflare/cf-identity-dynamic/6eaa2dec75a1b3e23eeecf6316daebc93520a4fa/src/img/cf_favicon.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | const root = ReactDOM.createRoot(document.getElementById('root'));
7 | root.render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { getAssetFromKV } from "@cloudflare/kv-asset-handler";
2 |
3 | /* eslint-disable */
4 |
5 | addEventListener("fetch", (event) => {
6 | const url = new URL(event.request.url);
7 |
8 | if (url.pathname === "/api/userdetails") {
9 | event.respondWith(handleUserDetails(event.request));
10 | } else if (url.pathname === "/api/history") {
11 | event.respondWith(handleHistoryRequest(event.request));
12 | } else if (url.pathname === "/api/debug") {
13 | event.respondWith(handleDebugPage(event.request));
14 | } else if (url.pathname === "/api/env") {
15 | event.respondWith(handleEnvRequest(event.request, event.env));
16 | } else if (url.pathname === "/api/upload") {
17 | event.respondWith(handleUploadRequest(event.request, event.env));
18 | } else if (url.pathname === "/assets/logo") {
19 | event.respondWith(handleAssetRetrieval(event.request, event.env, "logo"));
20 | } else {
21 | event.respondWith(handleEvent(event));
22 | }
23 | });
24 |
25 | // Expose worker env var via API endpoint (needed for frontend shenanigans)
26 | // This also has the theme since its stored in kv upon configuration
27 | const corsOrigin = CORS_ORIGIN;
28 | const corsHeaders = {
29 | "Access-Control-Allow-Origin": corsOrigin,
30 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
31 | "Access-Control-Allow-Headers": "Content-Type",
32 | };
33 |
34 | // serve the environment config (configured in wrangler.toml) for the components to reference
35 | // this includes worker settings, ui themeing
36 | async function handleEnvRequest(request, env) {
37 | if (request.method === "OPTIONS") {
38 | return new Response(null, { headers: corsHeaders });
39 | }
40 |
41 | const themeKey = "theme";
42 |
43 | try {
44 | if (request.method === "POST") {
45 | const body = await request.json();
46 |
47 | // Update to only store primary and secondary colors
48 | const updatedTheme = {
49 | primaryColor: body.primaryColor || "#3498db", // Default for primary color
50 | secondaryColor: body.secondaryColor || "#2ecc71", // Default for secondary color
51 | };
52 | // eslint-disable-next-line
53 | await IDENTITY_DYNAMIC_THEME_STORE.put(
54 | themeKey,
55 | JSON.stringify(updatedTheme)
56 | );
57 |
58 | return new Response(
59 | JSON.stringify({ message: "Theme updated successfully!" }),
60 | {
61 | headers: { ...corsHeaders, "Content-Type": "application/json" },
62 | }
63 | );
64 | }
65 |
66 | if (request.method === "GET") {
67 | const theme = await IDENTITY_DYNAMIC_THEME_STORE.get(themeKey);
68 |
69 | // Update environment variables to include only primary and secondary colors
70 | const envVars = {
71 | ORGANIZATION_ID: ORGANIZATION_ID,
72 | ORGANIZATION_NAME: ORGANIZATION_NAME,
73 | TARGET_GROUP: TARGET_GROUP,
74 | DEBUG: DEBUG,
75 | theme: theme
76 | ? JSON.parse(theme)
77 | : {
78 | primaryColor: "#3498db", // Default for primary color
79 | secondaryColor: "#2ecc71", // Default for secondary color
80 | },
81 | };
82 |
83 | return new Response(JSON.stringify(envVars), {
84 | headers: { ...corsHeaders, "Content-Type": "application/json" },
85 | });
86 | }
87 |
88 | return new Response("Method Not Allowed", {
89 | status: 405,
90 | headers: corsHeaders,
91 | });
92 | } catch (error) {
93 | console.error("Error handling /api/env:", error);
94 | return new Response(JSON.stringify({ error: "Internal Server Error" }), {
95 | status: 500,
96 | headers: { ...corsHeaders, "Content-Type": "application/json" },
97 | });
98 | }
99 | }
100 |
101 | /* eslint-enable */
102 |
103 | async function handleUploadRequest(request, env) {
104 | if (request.method === "OPTIONS") {
105 | return new Response(null, { headers: corsHeaders });
106 | }
107 |
108 | if (request.method !== "POST") {
109 | return new Response("Method Not Allowed", {
110 | status: 405,
111 | headers: corsHeaders,
112 | });
113 | }
114 |
115 | try {
116 | const contentType = request.headers.get("Content-Type");
117 | console.log("Content-Type:", contentType);
118 |
119 | if (!contentType || !contentType.includes("multipart/form-data")) {
120 | console.error("Invalid content type");
121 | return new Response(JSON.stringify({ error: "Invalid content type" }), {
122 | status: 400,
123 | headers: { ...corsHeaders, "Content-Type": "application/json" },
124 | });
125 | }
126 |
127 | const formData = await request.formData();
128 | console.log("Form data received:", formData);
129 |
130 | const file = formData.get("file");
131 | const type = formData.get("type");
132 |
133 | console.log("File:", file);
134 | console.log("Type:", type);
135 |
136 | if (!file || !type) {
137 | console.error("File or type missing in upload");
138 | return new Response(
139 | JSON.stringify({ error: "File or type missing in upload" }),
140 | {
141 | status: 400,
142 | headers: { ...corsHeaders, "Content-Type": "application/json" },
143 | }
144 | );
145 | }
146 |
147 | if (!["image/png", "image/jpeg", "image/svg+xml"].includes(file.type)) {
148 | console.error("Unsupported file type:", file.type);
149 | return new Response(JSON.stringify({ error: "Unsupported file type" }), {
150 | status: 400,
151 | headers: { ...corsHeaders, "Content-Type": "application/json" },
152 | });
153 | }
154 |
155 | // Save the file to KV
156 | const key = `${type}`;
157 | console.log(`Saving file to KV with key: ${key}`);
158 |
159 | await IDENTITY_DYNAMIC_THEME_STORE.put(key, file.stream(), {
160 | metadata: { contentType: file.type },
161 | });
162 |
163 | console.log("File uploaded successfully to KV:", key);
164 |
165 | return new Response(
166 | JSON.stringify({ message: `${type} uploaded successfully` }),
167 | {
168 | headers: { ...corsHeaders, "Content-Type": "application/json" },
169 | }
170 | );
171 | } catch (error) {
172 | console.error("Error handling image upload:", error);
173 | return new Response(
174 | JSON.stringify({
175 | error: "Failed to upload image",
176 | details: error.message,
177 | }),
178 | {
179 | status: 500,
180 | headers: { ...corsHeaders, "Content-Type": "application/json" },
181 | }
182 | );
183 | }
184 | }
185 |
186 | async function handleAssetRetrieval(request, env, key) {
187 | try {
188 | // Access the KV namespace via env
189 | const asset = await IDENTITY_DYNAMIC_THEME_STORE.getWithMetadata(key, {
190 | type: "stream",
191 | });
192 |
193 | if (!asset || !asset.value) {
194 | return new Response("Not Found", { status: 404 });
195 | }
196 |
197 | return new Response(asset.value, {
198 | headers: {
199 | "Content-Type":
200 | asset.metadata?.contentType || "application/octet-stream",
201 | },
202 | });
203 | } catch (error) {
204 | console.error(`Error retrieving asset "${key}":`, error);
205 | return new Response("Internal Server Error", { status: 500 });
206 | }
207 | }
208 |
209 | // handle the /api/debug endpoint
210 | async function handleDebugPage(request) {
211 | try {
212 | // Check if debugging is enabled
213 | const isDebugEnabled = String(DEBUG).toLowerCase() === "true";
214 |
215 | if (isDebugEnabled) {
216 | const identityResponse = await fetchIdentity(request);
217 |
218 | if (!identityResponse.ok) {
219 | return new Response(
220 | JSON.stringify({ error: "Failed to fetch identity." }),
221 | {
222 | status: identityResponse.status,
223 | headers: { "Content-Type": "application/json" },
224 | }
225 | );
226 | }
227 |
228 | const identityData = await identityResponse.json();
229 |
230 | return new Response(JSON.stringify(identityData), {
231 | headers: { "Content-Type": "application/json" },
232 | });
233 | } else {
234 | return new Response(JSON.stringify({ error: "Debugging is disabled." }), {
235 | status: 403,
236 | headers: { "Content-Type": "application/json" },
237 | });
238 | }
239 | } catch (error) {
240 | return new Response(
241 | JSON.stringify({ error: `Internal Server Error: ${error.message}` }),
242 | {
243 | status: 500,
244 | headers: { "Content-Type": "application/json" },
245 | }
246 | );
247 | }
248 | }
249 |
250 | // handle static pages
251 | async function handleEvent(event) {
252 | const url = new URL(event.request.url);
253 |
254 | try {
255 | if (url.pathname.startsWith("/assets/")) {
256 | const key = url.pathname.replace("/assets/", ""); // Extract the key
257 | const asset = await IDENTITY_DYNAMIC_THEME_STORE.getWithMetadata(key, {
258 | type: "stream",
259 | });
260 |
261 | if (!asset || !asset.value) {
262 | return new Response("Not Found", { status: 404 });
263 | }
264 |
265 | return new Response(asset.value, {
266 | headers: {
267 | "Content-Type":
268 | asset.metadata?.contentType || "application/octet-stream",
269 | },
270 | });
271 | }
272 |
273 | return await getAssetFromKV(event);
274 | } catch (error) {
275 | if (error.message.includes("could not find")) {
276 | const options = {
277 | mapRequestToAsset: (req) =>
278 | new Request(`${url.origin}/index.html`, req),
279 | };
280 | return await getAssetFromKV(event, options);
281 | }
282 | return new Response(error.message || "Unknown error", {
283 | status: error.status || 500,
284 | });
285 | }
286 | }
287 |
288 | // handle /api/userdetails + include user uuid for graphql
289 | async function handleUserDetails(request) {
290 | // Step 1: Attempt to get device_id directly from the token
291 | const accessCookie = request.headers.get("cf-access-jwt-assertion");
292 | if (!accessCookie) {
293 | return new Response(JSON.stringify({ error: "Unauthorized" }), {
294 | status: 401,
295 | });
296 | }
297 |
298 | // Try to extract device_id from the token
299 | let device_id = getDeviceIdFromToken(accessCookie);
300 |
301 | if (!device_id) {
302 | console.warn(
303 | "Device ID not found in token, attempting to fetch from get-identity"
304 | );
305 |
306 | // Step 2: Fallback - fetch identity data from get-identity endpoint to retrieve device_id
307 | const identityResponse = await fetchIdentity(request);
308 | if (!identityResponse.ok) {
309 | return identityResponse;
310 | }
311 |
312 | const identityData = await identityResponse.json();
313 | device_id = identityData?.identity?.device_id; // Get device_id from identity data
314 |
315 | if (!device_id) {
316 | return new Response(
317 | JSON.stringify({ error: "Device ID not found in identity data" }),
318 | { status: 400 }
319 | );
320 | }
321 | }
322 |
323 | try {
324 | // Proceed with the fetched or fallback device_id
325 | const identityResponse = await fetchIdentity(request);
326 | if (!identityResponse.ok) {
327 | return identityResponse;
328 | }
329 |
330 | const identityData = await identityResponse.json();
331 | const deviceDetailsResponse = await fetchDeviceDetails(
332 | identityData.gateway_account_id,
333 | device_id
334 | );
335 |
336 | let deviceDetailsData = {};
337 | if (deviceDetailsResponse.ok) {
338 | deviceDetailsData = await deviceDetailsResponse.json();
339 | } else if (deviceDetailsResponse.status === 404) {
340 | console.warn(
341 | `Device with ID ${device_id} not found (404). Device details unavailable.`
342 | );
343 | } else {
344 | return deviceDetailsResponse;
345 | }
346 |
347 | const devicePostureResponse = await fetchDevicePosture(
348 | identityData.gateway_account_id,
349 | device_id
350 | );
351 |
352 | let devicePostureData = {};
353 | if (devicePostureResponse.ok) {
354 | devicePostureData = await devicePostureResponse.json();
355 | } else {
356 | console.warn(
357 | `Device posture could not be retrieved for device ID ${device_id}.`
358 | );
359 | }
360 |
361 | const combinedData = {
362 | identity: identityData,
363 | device: deviceDetailsData,
364 | posture: devicePostureData,
365 | };
366 |
367 | return new Response(JSON.stringify(combinedData), {
368 | headers: { "Content-Type": "application/json" },
369 | });
370 | } catch (error) {
371 | console.error("Error in handleUserDetails:", error);
372 | return new Response(
373 | JSON.stringify({ error: `Internal Server Error: ${error.message}` }),
374 | {
375 | status: 500,
376 | headers: { "Content-Type": "application/json" },
377 | }
378 | );
379 | }
380 | }
381 |
382 | // This is to assist handleUserDetails - getting the deviceid directly from the cfauth cookie
383 | function getDeviceIdFromToken(jwt) {
384 | // eslint-disable-next-line
385 | const [header, payload, signature] = jwt.split(".");
386 | if (payload) {
387 | try {
388 | const decoded = JSON.parse(
389 | atob(payload.replace(/_/g, "/").replace(/-/g, "+"))
390 | );
391 | return decoded.device_id || null; // Return device_id or null if not found
392 | } catch (error) {
393 | console.error("Error decoding JWT for device_id extraction:", error);
394 | }
395 | }
396 | return null;
397 | }
398 |
399 | // get-identity
400 | async function fetchIdentity(request, retries = 1) {
401 | const accessCookie = request.headers.get("cf-access-jwt-assertion");
402 | if (!accessCookie) {
403 | return new Response(JSON.stringify({ error: "Unauthorized" }), {
404 | status: 401,
405 | });
406 | }
407 |
408 | // eslint-disable-next-line
409 | const url = `https://${ORGANIZATION_NAME}.cloudflareaccess.com/cdn-cgi/access/get-identity`;
410 |
411 | try {
412 | const response = await fetch(url, {
413 | method: "GET",
414 | headers: {
415 | "Content-Type": "application/json",
416 | Cookie: `CF_Authorization=${accessCookie}`,
417 | },
418 | });
419 |
420 | const textResponse = await response.text();
421 |
422 | // Check if the response is valid JSON
423 | try {
424 | const jsonResponse = JSON.parse(textResponse);
425 | return new Response(JSON.stringify(jsonResponse), {
426 | status: response.status,
427 | });
428 | } catch (e) {
429 | console.error("Received invalid JSON, attempting retry...", e);
430 |
431 | if (retries > 0) {
432 | // Retry fetchIdentity if response is not valid JSON
433 | return await fetchIdentity(request, retries - 1);
434 | } else {
435 | return new Response(
436 | JSON.stringify({ error: "Failed to fetch identity after retrying." }),
437 | { status: 500 }
438 | );
439 | }
440 | }
441 | } catch (error) {
442 | console.error("Error fetching identity:", error);
443 | return new Response(
444 | JSON.stringify({ error: `Failed to fetch identity: ${error.message}` }),
445 | {
446 | status: 500,
447 | headers: { "Content-Type": "application/json" },
448 | }
449 | );
450 | }
451 | }
452 |
453 | // api -> device information
454 | async function fetchDeviceDetails(gateway_account_id, device_id) {
455 | const url = `https://api.cloudflare.com/client/v4/accounts/${gateway_account_id}/devices/${device_id}`;
456 | console.log(`Attempting to fetch device details from URL: ${url}`); // Log the request URL
457 |
458 | try {
459 | const response = await fetch(url, {
460 | method: "GET",
461 | headers: {
462 | "Content-Type": "application/json",
463 | // eslint-disable-next-line
464 | Authorization: `Bearer ${BEARER_TOKEN}`,
465 | },
466 | });
467 |
468 | console.log(`Device details response status: ${response.status}`);
469 | if (!response.ok) {
470 | const errorText = await response.clone().text();
471 | console.error(
472 | `Failed to fetch device details for device_id ${device_id}:`,
473 | errorText
474 | );
475 | return response;
476 | }
477 |
478 | const deviceDetails = await response.json();
479 | console.log(
480 | `Fetched device details for device_id ${device_id}:`
481 | // deviceDetails
482 | );
483 |
484 | return new Response(JSON.stringify(deviceDetails), {
485 | status: 200,
486 | headers: { "Content-Type": "application/json" },
487 | });
488 | } catch (error) {
489 | console.error(
490 | `Error fetching device details for device_id ${device_id}:`,
491 | error.message
492 | );
493 | return new Response(
494 | JSON.stringify({ error: `Internal Server Error: ${error.message}` }),
495 | {
496 | status: 500,
497 | headers: { "Content-Type": "application/json" },
498 | }
499 | );
500 | }
501 | }
502 |
503 | // api -> posture information
504 | async function fetchDevicePosture(gateway_account_id, device_id) {
505 | const url = `https://api.cloudflare.com/client/v4/accounts/${gateway_account_id}/devices/${device_id}/posture/check?enrich=false`;
506 |
507 | try {
508 | console.log(`Fetching device posture for device_id ${device_id}`);
509 | const response = await fetch(url, {
510 | method: "GET",
511 | headers: {
512 | "Content-Type": "application/json",
513 | // eslint-disable-next-line
514 | Authorization: `Bearer ${BEARER_TOKEN}`,
515 | },
516 | });
517 |
518 | if (!response.ok) {
519 | console.error(
520 | `Failed to fetch device posture for device_id ${device_id}:`,
521 | await response.text()
522 | );
523 | return response;
524 | }
525 |
526 | const devicePosture = await response.json();
527 | console.log(
528 | `Fetched device posture for device_id ${device_id}:`
529 | // devicePosture
530 | );
531 |
532 | return new Response(JSON.stringify(devicePosture), {
533 | status: 200,
534 | headers: { "Content-Type": "application/json" },
535 | });
536 | } catch (error) {
537 | console.error(
538 | `Error fetching device posture for device_id ${device_id}:`,
539 | error.message
540 | );
541 | return new Response(
542 | JSON.stringify({ error: `Internal Server Error: ${error.message}` }),
543 | {
544 | status: 500,
545 | headers: { "Content-Type": "application/json" },
546 | }
547 | );
548 | }
549 | }
550 |
551 | // graphql
552 | // https://developers.cloudflare.com/analytics/graphql-api/tutorials/querying-access-login-events/
553 | async function handleHistoryRequest(request) {
554 | try {
555 | // Fetch user details to get `user_uuid` - will be used to filter
556 | const userDetailsResponse = await handleUserDetails(request);
557 | const userDetailsData = await userDetailsResponse.json();
558 | const userUuid = userDetailsData.identity?.user_uuid;
559 |
560 | if (!userUuid) {
561 | return new Response(JSON.stringify({ error: "user_uuid not found" }), {
562 | status: 400,
563 | });
564 | }
565 |
566 | /* eslint-disable */
567 |
568 | const query = `
569 | query {
570 | viewer {
571 | accounts(filter: {accountTag: "${ACCOUNT_ID}"}) {
572 | accessLoginRequestsAdaptiveGroups(
573 | limit: 5,
574 | filter: {
575 | datetime_geq: "${new Date(Date.now() - 10 * 60000).toISOString()}",
576 | datetime_leq: "${new Date().toISOString()}",
577 | userUuid: "${userUuid}",
578 | isSuccessfulLogin: 0
579 | },
580 | orderBy: [datetime_DESC]
581 | ) {
582 | dimensions {
583 | datetime
584 | isSuccessfulLogin
585 | hasWarpEnabled
586 | hasGatewayEnabled
587 | ipAddress
588 | userUuid
589 | identityProvider
590 | country
591 | deviceId
592 | mtlsStatus
593 | approvingPolicyId
594 | appId
595 | }
596 | }
597 | }
598 | }
599 | }`;
600 |
601 | // Send request to Cloudflare's GraphQL API
602 | const response = await fetch(
603 | "https://api.cloudflare.com/client/v4/graphql",
604 | {
605 | method: "POST",
606 | headers: {
607 | Authorization: `Bearer ${BEARER_TOKEN}`,
608 | "Content-Type": "application/json",
609 | },
610 | body: JSON.stringify({ query }),
611 | }
612 | );
613 |
614 | if (!response.ok) {
615 | const errorText = await response.text();
616 | console.error("Failed to fetch history data:", errorText);
617 | return new Response(
618 | JSON.stringify({ error: "Failed to fetch history data" }),
619 | { status: 500 }
620 | );
621 | }
622 |
623 | const data = await response.json();
624 | const loginEvents =
625 | data?.data?.viewer?.accounts[0]?.accessLoginRequestsAdaptiveGroups || [];
626 |
627 | // Now, because we only have appId in graphQL, make another api request to get the readable name
628 | const appNames = await Promise.all(
629 | loginEvents.map(async (event) => {
630 | const appId = event.dimensions.appId;
631 | if (appId) {
632 | const appUrl = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/apps/${appId}`;
633 | try {
634 | const appResponse = await fetch(appUrl, {
635 | method: "GET",
636 | headers: {
637 | Authorization: `Bearer ${BEARER_TOKEN}`,
638 | "Content-Type": "application/json",
639 | },
640 | });
641 |
642 | if (appResponse.ok) {
643 | const appData = await appResponse.json();
644 | // console.log("App Data:", appData);
645 | return appData.result?.name || "Unknown App";
646 | } else {
647 | console.error(`Failed to fetch app name for appId ${appId}`);
648 | return "Unknown App";
649 | }
650 | } catch (error) {
651 | console.error(`Error fetching app name for appId ${appId}:`, error);
652 | return "Unknown App";
653 | }
654 | }
655 | return "No AppId";
656 | })
657 | );
658 |
659 | // Append the name to entry in the history endpoint
660 | const enhancedLoginEvents = loginEvents.map((event, index) => ({
661 | ...event,
662 | applicationName: appNames[index],
663 | }));
664 | /* eslint-disable */
665 | return new Response(JSON.stringify({ loginHistory: enhancedLoginEvents }), {
666 | headers: { "Content-Type": "application/json" },
667 | });
668 | } catch (error) {
669 | console.error("Error in handleHistoryRequest:", error);
670 | return new Response(
671 | JSON.stringify({ error: `Internal Server Error: ${error.message}` }),
672 | {
673 | status: 500,
674 | headers: { "Content-Type": "application/json" },
675 | }
676 | );
677 | }
678 | }
679 |
--------------------------------------------------------------------------------
/src/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "worker",
3 | "version": "0.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "worker",
9 | "version": "0.0.0",
10 | "devDependencies": {
11 | "wrangler": "3.26.0"
12 | }
13 | },
14 | "node_modules/@cloudflare/kv-asset-handler": {
15 | "version": "0.2.0",
16 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz",
17 | "integrity": "sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==",
18 | "dev": true,
19 | "dependencies": {
20 | "mime": "^3.0.0"
21 | }
22 | },
23 | "node_modules/@cloudflare/workerd-darwin-64": {
24 | "version": "1.20240129.0",
25 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240129.0.tgz",
26 | "integrity": "sha512-DfVVB5IsQLVcWPJwV019vY3nEtU88c2Qu2ST5SQxqcGivZ52imagLRK0RHCIP8PK4piSiq90qUC6ybppUsw8eg==",
27 | "cpu": [
28 | "x64"
29 | ],
30 | "dev": true,
31 | "optional": true,
32 | "os": [
33 | "darwin"
34 | ],
35 | "engines": {
36 | "node": ">=16"
37 | }
38 | },
39 | "node_modules/@cloudflare/workerd-darwin-arm64": {
40 | "version": "1.20240129.0",
41 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240129.0.tgz",
42 | "integrity": "sha512-t0q8ABkmumG1zRM/MZ/vIv/Ysx0vTAXnQAPy/JW5aeQi/tqrypXkO9/NhPc0jbF/g/hIPrWEqpDgEp3CB7Da7Q==",
43 | "cpu": [
44 | "arm64"
45 | ],
46 | "dev": true,
47 | "optional": true,
48 | "os": [
49 | "darwin"
50 | ],
51 | "engines": {
52 | "node": ">=16"
53 | }
54 | },
55 | "node_modules/@cloudflare/workerd-linux-64": {
56 | "version": "1.20240129.0",
57 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240129.0.tgz",
58 | "integrity": "sha512-sFV1uobHgDI+6CKBS/ZshQvOvajgwl6BtiYaH4PSFSpvXTmRx+A9bcug+6BnD+V4WgwxTiEO2iR97E1XuwDAVw==",
59 | "cpu": [
60 | "x64"
61 | ],
62 | "dev": true,
63 | "optional": true,
64 | "os": [
65 | "linux"
66 | ],
67 | "engines": {
68 | "node": ">=16"
69 | }
70 | },
71 | "node_modules/@cloudflare/workerd-linux-arm64": {
72 | "version": "1.20240129.0",
73 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240129.0.tgz",
74 | "integrity": "sha512-O7q7htHaFRp8PgTqNJx1/fYc3+LnvAo6kWWB9a14C5OWak6AAZk42PNpKPx+DXTmGvI+8S1+futBGUeJ8NPDXg==",
75 | "cpu": [
76 | "arm64"
77 | ],
78 | "dev": true,
79 | "optional": true,
80 | "os": [
81 | "linux"
82 | ],
83 | "engines": {
84 | "node": ">=16"
85 | }
86 | },
87 | "node_modules/@cloudflare/workerd-windows-64": {
88 | "version": "1.20240129.0",
89 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240129.0.tgz",
90 | "integrity": "sha512-YqGno0XSqqqkDmNoGEX6M8kJlI2lEfWntbTPVtHaZlaXVR9sWfoD7TEno0NKC95cXFz+ioyFLbgbOdnfWwmVAA==",
91 | "cpu": [
92 | "x64"
93 | ],
94 | "dev": true,
95 | "optional": true,
96 | "os": [
97 | "win32"
98 | ],
99 | "engines": {
100 | "node": ">=16"
101 | }
102 | },
103 | "node_modules/@cspotcode/source-map-support": {
104 | "version": "0.8.1",
105 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
106 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
107 | "dev": true,
108 | "dependencies": {
109 | "@jridgewell/trace-mapping": "0.3.9"
110 | },
111 | "engines": {
112 | "node": ">=12"
113 | }
114 | },
115 | "node_modules/@esbuild-plugins/node-globals-polyfill": {
116 | "version": "0.2.3",
117 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
118 | "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==",
119 | "dev": true,
120 | "peerDependencies": {
121 | "esbuild": "*"
122 | }
123 | },
124 | "node_modules/@esbuild-plugins/node-modules-polyfill": {
125 | "version": "0.2.2",
126 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz",
127 | "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==",
128 | "dev": true,
129 | "dependencies": {
130 | "escape-string-regexp": "^4.0.0",
131 | "rollup-plugin-node-polyfills": "^0.2.1"
132 | },
133 | "peerDependencies": {
134 | "esbuild": "*"
135 | }
136 | },
137 | "node_modules/@esbuild/android-arm": {
138 | "version": "0.17.19",
139 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
140 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
141 | "cpu": [
142 | "arm"
143 | ],
144 | "dev": true,
145 | "optional": true,
146 | "os": [
147 | "android"
148 | ],
149 | "engines": {
150 | "node": ">=12"
151 | }
152 | },
153 | "node_modules/@esbuild/android-arm64": {
154 | "version": "0.17.19",
155 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
156 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
157 | "cpu": [
158 | "arm64"
159 | ],
160 | "dev": true,
161 | "optional": true,
162 | "os": [
163 | "android"
164 | ],
165 | "engines": {
166 | "node": ">=12"
167 | }
168 | },
169 | "node_modules/@esbuild/android-x64": {
170 | "version": "0.17.19",
171 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
172 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
173 | "cpu": [
174 | "x64"
175 | ],
176 | "dev": true,
177 | "optional": true,
178 | "os": [
179 | "android"
180 | ],
181 | "engines": {
182 | "node": ">=12"
183 | }
184 | },
185 | "node_modules/@esbuild/darwin-arm64": {
186 | "version": "0.17.19",
187 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
188 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
189 | "cpu": [
190 | "arm64"
191 | ],
192 | "dev": true,
193 | "optional": true,
194 | "os": [
195 | "darwin"
196 | ],
197 | "engines": {
198 | "node": ">=12"
199 | }
200 | },
201 | "node_modules/@esbuild/darwin-x64": {
202 | "version": "0.17.19",
203 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
204 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
205 | "cpu": [
206 | "x64"
207 | ],
208 | "dev": true,
209 | "optional": true,
210 | "os": [
211 | "darwin"
212 | ],
213 | "engines": {
214 | "node": ">=12"
215 | }
216 | },
217 | "node_modules/@esbuild/freebsd-arm64": {
218 | "version": "0.17.19",
219 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
220 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
221 | "cpu": [
222 | "arm64"
223 | ],
224 | "dev": true,
225 | "optional": true,
226 | "os": [
227 | "freebsd"
228 | ],
229 | "engines": {
230 | "node": ">=12"
231 | }
232 | },
233 | "node_modules/@esbuild/freebsd-x64": {
234 | "version": "0.17.19",
235 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
236 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
237 | "cpu": [
238 | "x64"
239 | ],
240 | "dev": true,
241 | "optional": true,
242 | "os": [
243 | "freebsd"
244 | ],
245 | "engines": {
246 | "node": ">=12"
247 | }
248 | },
249 | "node_modules/@esbuild/linux-arm": {
250 | "version": "0.17.19",
251 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
252 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
253 | "cpu": [
254 | "arm"
255 | ],
256 | "dev": true,
257 | "optional": true,
258 | "os": [
259 | "linux"
260 | ],
261 | "engines": {
262 | "node": ">=12"
263 | }
264 | },
265 | "node_modules/@esbuild/linux-arm64": {
266 | "version": "0.17.19",
267 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
268 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
269 | "cpu": [
270 | "arm64"
271 | ],
272 | "dev": true,
273 | "optional": true,
274 | "os": [
275 | "linux"
276 | ],
277 | "engines": {
278 | "node": ">=12"
279 | }
280 | },
281 | "node_modules/@esbuild/linux-ia32": {
282 | "version": "0.17.19",
283 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
284 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
285 | "cpu": [
286 | "ia32"
287 | ],
288 | "dev": true,
289 | "optional": true,
290 | "os": [
291 | "linux"
292 | ],
293 | "engines": {
294 | "node": ">=12"
295 | }
296 | },
297 | "node_modules/@esbuild/linux-loong64": {
298 | "version": "0.17.19",
299 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
300 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
301 | "cpu": [
302 | "loong64"
303 | ],
304 | "dev": true,
305 | "optional": true,
306 | "os": [
307 | "linux"
308 | ],
309 | "engines": {
310 | "node": ">=12"
311 | }
312 | },
313 | "node_modules/@esbuild/linux-mips64el": {
314 | "version": "0.17.19",
315 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
316 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
317 | "cpu": [
318 | "mips64el"
319 | ],
320 | "dev": true,
321 | "optional": true,
322 | "os": [
323 | "linux"
324 | ],
325 | "engines": {
326 | "node": ">=12"
327 | }
328 | },
329 | "node_modules/@esbuild/linux-ppc64": {
330 | "version": "0.17.19",
331 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
332 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
333 | "cpu": [
334 | "ppc64"
335 | ],
336 | "dev": true,
337 | "optional": true,
338 | "os": [
339 | "linux"
340 | ],
341 | "engines": {
342 | "node": ">=12"
343 | }
344 | },
345 | "node_modules/@esbuild/linux-riscv64": {
346 | "version": "0.17.19",
347 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
348 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
349 | "cpu": [
350 | "riscv64"
351 | ],
352 | "dev": true,
353 | "optional": true,
354 | "os": [
355 | "linux"
356 | ],
357 | "engines": {
358 | "node": ">=12"
359 | }
360 | },
361 | "node_modules/@esbuild/linux-s390x": {
362 | "version": "0.17.19",
363 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
364 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
365 | "cpu": [
366 | "s390x"
367 | ],
368 | "dev": true,
369 | "optional": true,
370 | "os": [
371 | "linux"
372 | ],
373 | "engines": {
374 | "node": ">=12"
375 | }
376 | },
377 | "node_modules/@esbuild/linux-x64": {
378 | "version": "0.17.19",
379 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
380 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
381 | "cpu": [
382 | "x64"
383 | ],
384 | "dev": true,
385 | "optional": true,
386 | "os": [
387 | "linux"
388 | ],
389 | "engines": {
390 | "node": ">=12"
391 | }
392 | },
393 | "node_modules/@esbuild/netbsd-x64": {
394 | "version": "0.17.19",
395 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
396 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
397 | "cpu": [
398 | "x64"
399 | ],
400 | "dev": true,
401 | "optional": true,
402 | "os": [
403 | "netbsd"
404 | ],
405 | "engines": {
406 | "node": ">=12"
407 | }
408 | },
409 | "node_modules/@esbuild/openbsd-x64": {
410 | "version": "0.17.19",
411 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
412 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
413 | "cpu": [
414 | "x64"
415 | ],
416 | "dev": true,
417 | "optional": true,
418 | "os": [
419 | "openbsd"
420 | ],
421 | "engines": {
422 | "node": ">=12"
423 | }
424 | },
425 | "node_modules/@esbuild/sunos-x64": {
426 | "version": "0.17.19",
427 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
428 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
429 | "cpu": [
430 | "x64"
431 | ],
432 | "dev": true,
433 | "optional": true,
434 | "os": [
435 | "sunos"
436 | ],
437 | "engines": {
438 | "node": ">=12"
439 | }
440 | },
441 | "node_modules/@esbuild/win32-arm64": {
442 | "version": "0.17.19",
443 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
444 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
445 | "cpu": [
446 | "arm64"
447 | ],
448 | "dev": true,
449 | "optional": true,
450 | "os": [
451 | "win32"
452 | ],
453 | "engines": {
454 | "node": ">=12"
455 | }
456 | },
457 | "node_modules/@esbuild/win32-ia32": {
458 | "version": "0.17.19",
459 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
460 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
461 | "cpu": [
462 | "ia32"
463 | ],
464 | "dev": true,
465 | "optional": true,
466 | "os": [
467 | "win32"
468 | ],
469 | "engines": {
470 | "node": ">=12"
471 | }
472 | },
473 | "node_modules/@esbuild/win32-x64": {
474 | "version": "0.17.19",
475 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
476 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
477 | "cpu": [
478 | "x64"
479 | ],
480 | "dev": true,
481 | "optional": true,
482 | "os": [
483 | "win32"
484 | ],
485 | "engines": {
486 | "node": ">=12"
487 | }
488 | },
489 | "node_modules/@fastify/busboy": {
490 | "version": "2.1.1",
491 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
492 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
493 | "dev": true,
494 | "engines": {
495 | "node": ">=14"
496 | }
497 | },
498 | "node_modules/@jridgewell/resolve-uri": {
499 | "version": "3.1.2",
500 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
501 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
502 | "dev": true,
503 | "engines": {
504 | "node": ">=6.0.0"
505 | }
506 | },
507 | "node_modules/@jridgewell/sourcemap-codec": {
508 | "version": "1.4.15",
509 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
510 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
511 | "dev": true
512 | },
513 | "node_modules/@jridgewell/trace-mapping": {
514 | "version": "0.3.9",
515 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
516 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
517 | "dev": true,
518 | "dependencies": {
519 | "@jridgewell/resolve-uri": "^3.0.3",
520 | "@jridgewell/sourcemap-codec": "^1.4.10"
521 | }
522 | },
523 | "node_modules/@types/node": {
524 | "version": "20.12.7",
525 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
526 | "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
527 | "dev": true,
528 | "dependencies": {
529 | "undici-types": "~5.26.4"
530 | }
531 | },
532 | "node_modules/@types/node-forge": {
533 | "version": "1.3.11",
534 | "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
535 | "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==",
536 | "dev": true,
537 | "dependencies": {
538 | "@types/node": "*"
539 | }
540 | },
541 | "node_modules/acorn": {
542 | "version": "8.11.3",
543 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
544 | "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
545 | "dev": true,
546 | "bin": {
547 | "acorn": "bin/acorn"
548 | },
549 | "engines": {
550 | "node": ">=0.4.0"
551 | }
552 | },
553 | "node_modules/acorn-walk": {
554 | "version": "8.3.2",
555 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
556 | "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
557 | "dev": true,
558 | "engines": {
559 | "node": ">=0.4.0"
560 | }
561 | },
562 | "node_modules/anymatch": {
563 | "version": "3.1.3",
564 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
565 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
566 | "dev": true,
567 | "dependencies": {
568 | "normalize-path": "^3.0.0",
569 | "picomatch": "^2.0.4"
570 | },
571 | "engines": {
572 | "node": ">= 8"
573 | }
574 | },
575 | "node_modules/as-table": {
576 | "version": "1.0.55",
577 | "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz",
578 | "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==",
579 | "dev": true,
580 | "dependencies": {
581 | "printable-characters": "^1.0.42"
582 | }
583 | },
584 | "node_modules/binary-extensions": {
585 | "version": "2.3.0",
586 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
587 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
588 | "dev": true,
589 | "engines": {
590 | "node": ">=8"
591 | },
592 | "funding": {
593 | "url": "https://github.com/sponsors/sindresorhus"
594 | }
595 | },
596 | "node_modules/blake3-wasm": {
597 | "version": "2.1.5",
598 | "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
599 | "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
600 | "dev": true
601 | },
602 | "node_modules/braces": {
603 | "version": "3.0.2",
604 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
605 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
606 | "dev": true,
607 | "dependencies": {
608 | "fill-range": "^7.0.1"
609 | },
610 | "engines": {
611 | "node": ">=8"
612 | }
613 | },
614 | "node_modules/capnp-ts": {
615 | "version": "0.7.0",
616 | "resolved": "https://registry.npmjs.org/capnp-ts/-/capnp-ts-0.7.0.tgz",
617 | "integrity": "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==",
618 | "dev": true,
619 | "dependencies": {
620 | "debug": "^4.3.1",
621 | "tslib": "^2.2.0"
622 | }
623 | },
624 | "node_modules/chokidar": {
625 | "version": "3.6.0",
626 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
627 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
628 | "dev": true,
629 | "dependencies": {
630 | "anymatch": "~3.1.2",
631 | "braces": "~3.0.2",
632 | "glob-parent": "~5.1.2",
633 | "is-binary-path": "~2.1.0",
634 | "is-glob": "~4.0.1",
635 | "normalize-path": "~3.0.0",
636 | "readdirp": "~3.6.0"
637 | },
638 | "engines": {
639 | "node": ">= 8.10.0"
640 | },
641 | "funding": {
642 | "url": "https://paulmillr.com/funding/"
643 | },
644 | "optionalDependencies": {
645 | "fsevents": "~2.3.2"
646 | }
647 | },
648 | "node_modules/cookie": {
649 | "version": "0.5.0",
650 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
651 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
652 | "dev": true,
653 | "engines": {
654 | "node": ">= 0.6"
655 | }
656 | },
657 | "node_modules/data-uri-to-buffer": {
658 | "version": "2.0.2",
659 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
660 | "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
661 | "dev": true
662 | },
663 | "node_modules/debug": {
664 | "version": "4.3.4",
665 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
666 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
667 | "dev": true,
668 | "dependencies": {
669 | "ms": "2.1.2"
670 | },
671 | "engines": {
672 | "node": ">=6.0"
673 | },
674 | "peerDependenciesMeta": {
675 | "supports-color": {
676 | "optional": true
677 | }
678 | }
679 | },
680 | "node_modules/esbuild": {
681 | "version": "0.17.19",
682 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
683 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
684 | "dev": true,
685 | "hasInstallScript": true,
686 | "bin": {
687 | "esbuild": "bin/esbuild"
688 | },
689 | "engines": {
690 | "node": ">=12"
691 | },
692 | "optionalDependencies": {
693 | "@esbuild/android-arm": "0.17.19",
694 | "@esbuild/android-arm64": "0.17.19",
695 | "@esbuild/android-x64": "0.17.19",
696 | "@esbuild/darwin-arm64": "0.17.19",
697 | "@esbuild/darwin-x64": "0.17.19",
698 | "@esbuild/freebsd-arm64": "0.17.19",
699 | "@esbuild/freebsd-x64": "0.17.19",
700 | "@esbuild/linux-arm": "0.17.19",
701 | "@esbuild/linux-arm64": "0.17.19",
702 | "@esbuild/linux-ia32": "0.17.19",
703 | "@esbuild/linux-loong64": "0.17.19",
704 | "@esbuild/linux-mips64el": "0.17.19",
705 | "@esbuild/linux-ppc64": "0.17.19",
706 | "@esbuild/linux-riscv64": "0.17.19",
707 | "@esbuild/linux-s390x": "0.17.19",
708 | "@esbuild/linux-x64": "0.17.19",
709 | "@esbuild/netbsd-x64": "0.17.19",
710 | "@esbuild/openbsd-x64": "0.17.19",
711 | "@esbuild/sunos-x64": "0.17.19",
712 | "@esbuild/win32-arm64": "0.17.19",
713 | "@esbuild/win32-ia32": "0.17.19",
714 | "@esbuild/win32-x64": "0.17.19"
715 | }
716 | },
717 | "node_modules/escape-string-regexp": {
718 | "version": "4.0.0",
719 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
720 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
721 | "dev": true,
722 | "engines": {
723 | "node": ">=10"
724 | },
725 | "funding": {
726 | "url": "https://github.com/sponsors/sindresorhus"
727 | }
728 | },
729 | "node_modules/estree-walker": {
730 | "version": "0.6.1",
731 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
732 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
733 | "dev": true
734 | },
735 | "node_modules/exit-hook": {
736 | "version": "2.2.1",
737 | "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
738 | "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
739 | "dev": true,
740 | "engines": {
741 | "node": ">=6"
742 | },
743 | "funding": {
744 | "url": "https://github.com/sponsors/sindresorhus"
745 | }
746 | },
747 | "node_modules/fill-range": {
748 | "version": "7.0.1",
749 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
750 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
751 | "dev": true,
752 | "dependencies": {
753 | "to-regex-range": "^5.0.1"
754 | },
755 | "engines": {
756 | "node": ">=8"
757 | }
758 | },
759 | "node_modules/fsevents": {
760 | "version": "2.3.3",
761 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
762 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
763 | "dev": true,
764 | "hasInstallScript": true,
765 | "optional": true,
766 | "os": [
767 | "darwin"
768 | ],
769 | "engines": {
770 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
771 | }
772 | },
773 | "node_modules/function-bind": {
774 | "version": "1.1.2",
775 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
776 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
777 | "dev": true,
778 | "funding": {
779 | "url": "https://github.com/sponsors/ljharb"
780 | }
781 | },
782 | "node_modules/get-source": {
783 | "version": "2.0.12",
784 | "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
785 | "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==",
786 | "dev": true,
787 | "dependencies": {
788 | "data-uri-to-buffer": "^2.0.0",
789 | "source-map": "^0.6.1"
790 | }
791 | },
792 | "node_modules/glob-parent": {
793 | "version": "5.1.2",
794 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
795 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
796 | "dev": true,
797 | "dependencies": {
798 | "is-glob": "^4.0.1"
799 | },
800 | "engines": {
801 | "node": ">= 6"
802 | }
803 | },
804 | "node_modules/glob-to-regexp": {
805 | "version": "0.4.1",
806 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
807 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
808 | "dev": true
809 | },
810 | "node_modules/hasown": {
811 | "version": "2.0.2",
812 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
813 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
814 | "dev": true,
815 | "dependencies": {
816 | "function-bind": "^1.1.2"
817 | },
818 | "engines": {
819 | "node": ">= 0.4"
820 | }
821 | },
822 | "node_modules/is-binary-path": {
823 | "version": "2.1.0",
824 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
825 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
826 | "dev": true,
827 | "dependencies": {
828 | "binary-extensions": "^2.0.0"
829 | },
830 | "engines": {
831 | "node": ">=8"
832 | }
833 | },
834 | "node_modules/is-core-module": {
835 | "version": "2.13.1",
836 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
837 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
838 | "dev": true,
839 | "dependencies": {
840 | "hasown": "^2.0.0"
841 | },
842 | "funding": {
843 | "url": "https://github.com/sponsors/ljharb"
844 | }
845 | },
846 | "node_modules/is-extglob": {
847 | "version": "2.1.1",
848 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
849 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
850 | "dev": true,
851 | "engines": {
852 | "node": ">=0.10.0"
853 | }
854 | },
855 | "node_modules/is-glob": {
856 | "version": "4.0.3",
857 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
858 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
859 | "dev": true,
860 | "dependencies": {
861 | "is-extglob": "^2.1.1"
862 | },
863 | "engines": {
864 | "node": ">=0.10.0"
865 | }
866 | },
867 | "node_modules/is-number": {
868 | "version": "7.0.0",
869 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
870 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
871 | "dev": true,
872 | "engines": {
873 | "node": ">=0.12.0"
874 | }
875 | },
876 | "node_modules/magic-string": {
877 | "version": "0.25.9",
878 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
879 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
880 | "dev": true,
881 | "dependencies": {
882 | "sourcemap-codec": "^1.4.8"
883 | }
884 | },
885 | "node_modules/mime": {
886 | "version": "3.0.0",
887 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
888 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
889 | "dev": true,
890 | "bin": {
891 | "mime": "cli.js"
892 | },
893 | "engines": {
894 | "node": ">=10.0.0"
895 | }
896 | },
897 | "node_modules/miniflare": {
898 | "version": "3.20240129.0",
899 | "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240129.0.tgz",
900 | "integrity": "sha512-27pDhlP2G/4gXmvnSt6LjMQ8KrkmbJElIQmn+BLjdiyIx+zXY4E8MSPJmi9flgf0dn3wtjuHO2ASenuopqqxrw==",
901 | "dev": true,
902 | "dependencies": {
903 | "@cspotcode/source-map-support": "0.8.1",
904 | "acorn": "^8.8.0",
905 | "acorn-walk": "^8.2.0",
906 | "capnp-ts": "^0.7.0",
907 | "exit-hook": "^2.2.1",
908 | "glob-to-regexp": "^0.4.1",
909 | "stoppable": "^1.1.0",
910 | "undici": "^5.28.2",
911 | "workerd": "1.20240129.0",
912 | "ws": "^8.11.0",
913 | "youch": "^3.2.2",
914 | "zod": "^3.20.6"
915 | },
916 | "bin": {
917 | "miniflare": "bootstrap.js"
918 | },
919 | "engines": {
920 | "node": ">=16.13"
921 | }
922 | },
923 | "node_modules/ms": {
924 | "version": "2.1.2",
925 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
926 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
927 | "dev": true
928 | },
929 | "node_modules/mustache": {
930 | "version": "4.2.0",
931 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
932 | "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
933 | "dev": true,
934 | "bin": {
935 | "mustache": "bin/mustache"
936 | }
937 | },
938 | "node_modules/nanoid": {
939 | "version": "3.3.7",
940 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
941 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
942 | "dev": true,
943 | "funding": [
944 | {
945 | "type": "github",
946 | "url": "https://github.com/sponsors/ai"
947 | }
948 | ],
949 | "bin": {
950 | "nanoid": "bin/nanoid.cjs"
951 | },
952 | "engines": {
953 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
954 | }
955 | },
956 | "node_modules/node-forge": {
957 | "version": "1.3.1",
958 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
959 | "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
960 | "dev": true,
961 | "engines": {
962 | "node": ">= 6.13.0"
963 | }
964 | },
965 | "node_modules/normalize-path": {
966 | "version": "3.0.0",
967 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
968 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
969 | "dev": true,
970 | "engines": {
971 | "node": ">=0.10.0"
972 | }
973 | },
974 | "node_modules/path-parse": {
975 | "version": "1.0.7",
976 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
977 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
978 | "dev": true
979 | },
980 | "node_modules/path-to-regexp": {
981 | "version": "6.2.2",
982 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
983 | "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
984 | "dev": true
985 | },
986 | "node_modules/picomatch": {
987 | "version": "2.3.1",
988 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
989 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
990 | "dev": true,
991 | "engines": {
992 | "node": ">=8.6"
993 | },
994 | "funding": {
995 | "url": "https://github.com/sponsors/jonschlinkert"
996 | }
997 | },
998 | "node_modules/printable-characters": {
999 | "version": "1.0.42",
1000 | "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
1001 | "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
1002 | "dev": true
1003 | },
1004 | "node_modules/readdirp": {
1005 | "version": "3.6.0",
1006 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1007 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1008 | "dev": true,
1009 | "dependencies": {
1010 | "picomatch": "^2.2.1"
1011 | },
1012 | "engines": {
1013 | "node": ">=8.10.0"
1014 | }
1015 | },
1016 | "node_modules/resolve": {
1017 | "version": "1.22.8",
1018 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
1019 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
1020 | "dev": true,
1021 | "dependencies": {
1022 | "is-core-module": "^2.13.0",
1023 | "path-parse": "^1.0.7",
1024 | "supports-preserve-symlinks-flag": "^1.0.0"
1025 | },
1026 | "bin": {
1027 | "resolve": "bin/resolve"
1028 | },
1029 | "funding": {
1030 | "url": "https://github.com/sponsors/ljharb"
1031 | }
1032 | },
1033 | "node_modules/resolve.exports": {
1034 | "version": "2.0.2",
1035 | "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
1036 | "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==",
1037 | "dev": true,
1038 | "engines": {
1039 | "node": ">=10"
1040 | }
1041 | },
1042 | "node_modules/rollup-plugin-inject": {
1043 | "version": "3.0.2",
1044 | "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
1045 | "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==",
1046 | "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.",
1047 | "dev": true,
1048 | "dependencies": {
1049 | "estree-walker": "^0.6.1",
1050 | "magic-string": "^0.25.3",
1051 | "rollup-pluginutils": "^2.8.1"
1052 | }
1053 | },
1054 | "node_modules/rollup-plugin-node-polyfills": {
1055 | "version": "0.2.1",
1056 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz",
1057 | "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==",
1058 | "dev": true,
1059 | "dependencies": {
1060 | "rollup-plugin-inject": "^3.0.0"
1061 | }
1062 | },
1063 | "node_modules/rollup-pluginutils": {
1064 | "version": "2.8.2",
1065 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
1066 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
1067 | "dev": true,
1068 | "dependencies": {
1069 | "estree-walker": "^0.6.1"
1070 | }
1071 | },
1072 | "node_modules/selfsigned": {
1073 | "version": "2.4.1",
1074 | "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",
1075 | "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==",
1076 | "dev": true,
1077 | "dependencies": {
1078 | "@types/node-forge": "^1.3.0",
1079 | "node-forge": "^1"
1080 | },
1081 | "engines": {
1082 | "node": ">=10"
1083 | }
1084 | },
1085 | "node_modules/source-map": {
1086 | "version": "0.6.1",
1087 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1088 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
1089 | "dev": true,
1090 | "engines": {
1091 | "node": ">=0.10.0"
1092 | }
1093 | },
1094 | "node_modules/sourcemap-codec": {
1095 | "version": "1.4.8",
1096 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
1097 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
1098 | "deprecated": "Please use @jridgewell/sourcemap-codec instead",
1099 | "dev": true
1100 | },
1101 | "node_modules/stacktracey": {
1102 | "version": "2.1.8",
1103 | "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz",
1104 | "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==",
1105 | "dev": true,
1106 | "dependencies": {
1107 | "as-table": "^1.0.36",
1108 | "get-source": "^2.0.12"
1109 | }
1110 | },
1111 | "node_modules/stoppable": {
1112 | "version": "1.1.0",
1113 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
1114 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
1115 | "dev": true,
1116 | "engines": {
1117 | "node": ">=4",
1118 | "npm": ">=6"
1119 | }
1120 | },
1121 | "node_modules/supports-preserve-symlinks-flag": {
1122 | "version": "1.0.0",
1123 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1124 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1125 | "dev": true,
1126 | "engines": {
1127 | "node": ">= 0.4"
1128 | },
1129 | "funding": {
1130 | "url": "https://github.com/sponsors/ljharb"
1131 | }
1132 | },
1133 | "node_modules/to-regex-range": {
1134 | "version": "5.0.1",
1135 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1136 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1137 | "dev": true,
1138 | "dependencies": {
1139 | "is-number": "^7.0.0"
1140 | },
1141 | "engines": {
1142 | "node": ">=8.0"
1143 | }
1144 | },
1145 | "node_modules/tslib": {
1146 | "version": "2.6.2",
1147 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
1148 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
1149 | "dev": true
1150 | },
1151 | "node_modules/undici": {
1152 | "version": "5.28.4",
1153 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
1154 | "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
1155 | "dev": true,
1156 | "dependencies": {
1157 | "@fastify/busboy": "^2.0.0"
1158 | },
1159 | "engines": {
1160 | "node": ">=14.0"
1161 | }
1162 | },
1163 | "node_modules/undici-types": {
1164 | "version": "5.26.5",
1165 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1166 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
1167 | "dev": true
1168 | },
1169 | "node_modules/workerd": {
1170 | "version": "1.20240129.0",
1171 | "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240129.0.tgz",
1172 | "integrity": "sha512-t4pnsmjjk/u+GdVDgH2M1AFmJaBUABshYK/vT/HNrAXsHSwN6VR8Yqw0JQ845OokO34VLkuUtYQYyxHHKpdtsw==",
1173 | "dev": true,
1174 | "hasInstallScript": true,
1175 | "bin": {
1176 | "workerd": "bin/workerd"
1177 | },
1178 | "engines": {
1179 | "node": ">=16"
1180 | },
1181 | "optionalDependencies": {
1182 | "@cloudflare/workerd-darwin-64": "1.20240129.0",
1183 | "@cloudflare/workerd-darwin-arm64": "1.20240129.0",
1184 | "@cloudflare/workerd-linux-64": "1.20240129.0",
1185 | "@cloudflare/workerd-linux-arm64": "1.20240129.0",
1186 | "@cloudflare/workerd-windows-64": "1.20240129.0"
1187 | }
1188 | },
1189 | "node_modules/wrangler": {
1190 | "version": "3.26.0",
1191 | "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.26.0.tgz",
1192 | "integrity": "sha512-2FKDyL0wV6ws+9AHkQl5/Yzn17kG9jlpgyT7wqCDkhb5q+TCL/I8N5IKVwXe8tRrTluBI1QQZRRymoA5nu0pHw==",
1193 | "dev": true,
1194 | "dependencies": {
1195 | "@cloudflare/kv-asset-handler": "^0.2.0",
1196 | "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
1197 | "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
1198 | "blake3-wasm": "^2.1.5",
1199 | "chokidar": "^3.5.3",
1200 | "esbuild": "0.17.19",
1201 | "miniflare": "3.20240129.0",
1202 | "nanoid": "^3.3.3",
1203 | "path-to-regexp": "^6.2.0",
1204 | "resolve": "^1.22.8",
1205 | "resolve.exports": "^2.0.2",
1206 | "selfsigned": "^2.0.1",
1207 | "source-map": "0.6.1",
1208 | "xxhash-wasm": "^1.0.1"
1209 | },
1210 | "bin": {
1211 | "wrangler": "bin/wrangler.js",
1212 | "wrangler2": "bin/wrangler.js"
1213 | },
1214 | "engines": {
1215 | "node": ">=16.17.0"
1216 | },
1217 | "optionalDependencies": {
1218 | "fsevents": "~2.3.2"
1219 | }
1220 | },
1221 | "node_modules/ws": {
1222 | "version": "8.16.0",
1223 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
1224 | "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
1225 | "dev": true,
1226 | "engines": {
1227 | "node": ">=10.0.0"
1228 | },
1229 | "peerDependencies": {
1230 | "bufferutil": "^4.0.1",
1231 | "utf-8-validate": ">=5.0.2"
1232 | },
1233 | "peerDependenciesMeta": {
1234 | "bufferutil": {
1235 | "optional": true
1236 | },
1237 | "utf-8-validate": {
1238 | "optional": true
1239 | }
1240 | }
1241 | },
1242 | "node_modules/xxhash-wasm": {
1243 | "version": "1.0.2",
1244 | "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz",
1245 | "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==",
1246 | "dev": true
1247 | },
1248 | "node_modules/youch": {
1249 | "version": "3.3.3",
1250 | "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.3.tgz",
1251 | "integrity": "sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==",
1252 | "dev": true,
1253 | "dependencies": {
1254 | "cookie": "^0.5.0",
1255 | "mustache": "^4.2.0",
1256 | "stacktracey": "^2.1.8"
1257 | }
1258 | },
1259 | "node_modules/zod": {
1260 | "version": "3.22.5",
1261 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.5.tgz",
1262 | "integrity": "sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==",
1263 | "dev": true,
1264 | "funding": {
1265 | "url": "https://github.com/sponsors/colinhacks"
1266 | }
1267 | }
1268 | }
1269 | }
1270 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "worker",
3 | "version": "0.0.0",
4 | "devDependencies": {
5 | "wrangler": "3.26.0"
6 | },
7 | "private": true,
8 | "scripts": {
9 | "start": "wrangler dev",
10 | "deploy": "wrangler deploy"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/**/*.{js,jsx,ts,tsx}",
5 | ],
6 | theme: {
7 | colors: {
8 | 'blue': '#d9edf7',
9 | 'CForange': '#ff7900',
10 | 'purple': '#7e5bef',
11 | 'pink': '#ff49db',
12 | 'orange': '#c8600c',
13 | 'green': '#13ce66',
14 | 'yellow': '#fcf8e3',
15 | 'gray-dark': '#273444',
16 | 'gray': '#e7e9ee',
17 | 'gray-light': '#e1e4e9',
18 | 'white': '#ffffff',
19 | 'alertblue': '#31708f',
20 | 'steel' : '#f7f7f8',
21 | 'alertred' : '#f2dede',
22 | 'red' : '#E72929',
23 | 'dark-red' :'#b62226',
24 | 'warning' : '#8a6d3a',
25 | 'alert-green' : '#88f78c',
26 | 'alert-green2' : '#3d803f',
27 | 'gunmetal' : '#292C36',
28 | 'roman-silver' : '#8E99AC',
29 | 'silver-sand' : '#BDC2C7',
30 | 'blue-silver' : '#2b2b34',
31 | link: '#ff7901',
32 | },
33 | variants: {
34 | extend: {
35 | textColor: ['hover'],
36 | backgroundColor: ['hover'],
37 | },
38 | },
39 | fontFamily: {
40 | sans: ['Graphik', 'sans-serif'],
41 | serif: ['Merriweather', 'serif'],
42 | },
43 | extend: {
44 | spacing: {
45 | '8xl': '96rem',
46 | '9xl': '128rem',
47 | },
48 | borderRadius: {
49 | '4xl': '2rem',
50 | }
51 | }
52 | },
53 | }
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "identity-dynamic"
2 | account_id = ""
3 | workers_dev = false
4 | compatibility_date = "2024-11-25"
5 | main = "src/main.js"
6 |
7 |
8 | routes = [
9 | { pattern = "", custom_domain = true }
10 | ]
11 |
12 | # wrangler kv:namespace create IDENTITY_DYNAMIC_THEME_STORE
13 |
14 | [[kv_namespaces]]
15 | binding = "IDENTITY_DYNAMIC_THEME_STORE"
16 | id = "" # unique ID of the previously created namespace
17 |
18 | [vars]
19 | # - BEARER_TOKEN (Defined with wrangler secret put)
20 | CORS_ORIGIN = "" # This should be the route pattern domain /debug (https://example.com/debug)
21 | ACCOUNT_ID = ""
22 | ORGANIZATION_ID = ""
23 | ORGANIZATION_NAME = ""
24 | DEBUG = "true"
25 | TARGET_GROUP = "" # Define the "special group" that you want to use for notification
26 |
27 | [site]
28 | bucket = "./build"
29 |
30 | [build]
31 | command = "npm run build"
--------------------------------------------------------------------------------