├── .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 | example page 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 | * ![](assets/20241126_113713_image.png) 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 | * ![](assets/20241119_184227_image.png) 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 | 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 |
106 |

107 | Consult the information below for further details surrounding the 108 | above status information, for further assistance: 109 |

110 | 111 | 114 | 115 |
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 | 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 | 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 | 73 | 74 | 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 | 87 | 88 | 91 | 92 | 93 | 94 | 95 | {loginHistory.map((entry, index) => ( 96 | 97 | 98 | 99 | 100 | 107 | 108 | ))} 109 | 110 |
DateTime 89 | Application Name 90 | Reason
{entry.date}{entry.time}{entry.applicationName} 101 | 104 | {entry.reason.label} 105 | 106 |
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 | 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 | 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 | 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 | 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 | Uploaded Logo 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 | 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 | 48 | 49 | ) : ( 50 | 51 | 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 | 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 | 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 | 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" --------------------------------------------------------------------------------