├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ └── open_an_issue.md ├── aggregate.ts ├── config.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build-and-publish-github-pages.yml │ ├── gateway-conformance.yml │ ├── generated-pr.yml │ ├── js-test-and-release.yml │ └── stale.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── gateways.json ├── package-lock.json ├── package.json ├── scripts ├── beautify.mjs ├── build-sw.mjs └── test-gateways.mjs ├── src ├── CheckBase.ts ├── Checker.ts ├── Cors.ts ├── Flag.ts ├── GatewayNode.ts ├── Ipns.ts ├── Log.ts ├── Origin.ts ├── Results.ts ├── Stats.ts ├── Status.ts ├── Tag.ts ├── TagStatus.ts ├── Trustless.ts ├── UiComponent.ts ├── checkViaImgSrc.ts ├── constants.ts ├── expectSubdomainRedirect.ts ├── gatewayHostname.ts ├── global.d.ts ├── index.html ├── index.ts ├── metrics.ts ├── report.json ├── styles.css └── types.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "ipfs", 4 | "plugin:compat/recommended" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": false 9 | }, 10 | "rules": { 11 | "comma-dangle": ["error", "always-multiline"], 12 | "@typescript-eslint/comma-dangle": ["error", "always-multiline"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence 5 | # these owners will be requested for review when someone 6 | # opens a pull request. 7 | # All GUI Teams: @ipfs-shipyard/ipfs-gui @ipfs-shipyard/gui @ipfs/gui-dev @ipfs/wg-gui-ux 8 | * @ipfs/gui-dev 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Getting Help on IPFS 4 | url: https://ipfs.io/help 5 | about: All information about how and where to get help on IPFS. 6 | - name: IPFS Official Forum 7 | url: https://discuss.ipfs.io 8 | about: Please post general questions, support requests, and discussions here. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/open_an_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Open an issue 3 | about: Only for actionable issues relevant to this repository. 4 | title: '' 5 | labels: need/triage 6 | assignees: '' 7 | 8 | --- 9 | 20 | -------------------------------------------------------------------------------- /.github/aggregate.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import process from 'process' 3 | import path from 'path' 4 | import { z } from 'zod' 5 | 6 | interface ReportOutput { 7 | metadata: { 8 | time: string 9 | version?: string 10 | job_url?: string 11 | gateway_url: string 12 | } 13 | results: { 14 | [key: string]: { 15 | pass: number 16 | fail: number 17 | skip: number 18 | } 19 | } 20 | } 21 | 22 | type GatewayURL = string 23 | 24 | // At the moment test2json is likely to miss some outputs. 25 | // We'll accept "Unknown" as a valid outcome for now. 26 | // Related: https://github.com/golang/go/issues/61767 27 | const Outcome = z.enum(['pass', 'fail', 'skip', 'Unknown']) 28 | 29 | const ReportFileInput = z.intersection( 30 | z.record(z.object({ 31 | path: z.array(z.string()), 32 | time: z.string(), 33 | outcome: Outcome, 34 | output: z.string().optional(), 35 | meta: z.object({ 36 | group: z.string().optional(), 37 | }).optional(), 38 | })), 39 | z.object({ 40 | TestMetadata: z.object({ 41 | time: z.string(), 42 | meta: z.object({ 43 | version: z.string().optional(), 44 | job_url: z.string().optional(), 45 | gateway_url: z.string(), 46 | }) 47 | }).optional(), 48 | }) 49 | ) 50 | 51 | /** 52 | * Processes a report from a given filePath and extracts important data. 53 | */ 54 | const processReport = (filePath: string): [GatewayURL, ReportOutput] => { 55 | const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath); 56 | const { TestMetadata, ...allOtherTests } = ReportFileInput.parse(JSON.parse(fs.readFileSync(resolvedPath, 'utf8'))) 57 | 58 | 59 | if (!TestMetadata) { 60 | throw new Error(`No TestMetadata found in ${resolvedPath}`) 61 | } 62 | 63 | const { time, meta } = TestMetadata 64 | const { version, job_url, gateway_url } = meta 65 | 66 | // Then extract the test results we care about. 67 | const groups = Object.entries(allOtherTests) 68 | .filter(([_, value]) => value.path.length === 1) // keep only the tests at the root 69 | .map(([_key, value]) => { 70 | // keep only the outcomes and groups 71 | return { 72 | outcome: value.outcome, 73 | group: value.meta?.group ?? 'Others', 74 | } 75 | }) 76 | .reduce((acc, value) => { 77 | // then group by "group" value and sum their outcomes 78 | const { group } = value 79 | const outcome = value.outcome === 'Unknown' ? 'fail' : value.outcome 80 | 81 | if (!acc[group]) { 82 | acc[group] = { 83 | pass: 0, 84 | fail: 0, 85 | skip: 0, 86 | } 87 | } 88 | 89 | acc[group][outcome] += 1 90 | 91 | return acc 92 | }, {} as ReportOutput['results']) 93 | 94 | return [ 95 | gateway_url, 96 | { 97 | metadata: { 98 | time, version, job_url, gateway_url, 99 | }, 100 | results: groups, 101 | }, 102 | ] 103 | } 104 | 105 | /** 106 | * Main function to process all input files and write the results to standard output. 107 | */ 108 | const main = async (): Promise => { 109 | const inputs: string[] = process.argv.slice(2) // List of json reports to aggregate. 110 | 111 | const results: {[key: string]: ReportOutput} = {} 112 | 113 | inputs.forEach((filePath) => { 114 | try { 115 | const [name, report] = processReport(filePath) 116 | results[name] = report 117 | } catch (err) { 118 | console.error(`Error processing ${filePath}`, err) 119 | } 120 | }) 121 | 122 | fs.writeFileSync(1, JSON.stringify(results, null, 2) + '\n') 123 | } 124 | 125 | main() 126 | .then(() => process.exit(0)) 127 | .catch((err) => { 128 | console.error(err) 129 | process.exit(1) 130 | }) 131 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for welcome - https://github.com/behaviorbot/welcome 2 | 3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 4 | # Comment to be posted to on first time issues 5 | newIssueWelcomeComment: > 6 | Thank you for submitting your first issue to this repository! A maintainer 7 | will be here shortly to triage and review. 8 | 9 | In the meantime, please double-check that you have provided all the 10 | necessary information to make this process easy! Any information that can 11 | help save additional round trips is useful! We currently aim to give 12 | initial feedback within **two business days**. If this does not happen, feel 13 | free to leave a comment. 14 | 15 | Please keep an eye on how this issue will be labeled, as labels give an 16 | overview of priorities, assignments and additional actions requested by the 17 | maintainers: 18 | 19 | - "Priority" labels will show how urgent this is for the team. 20 | - "Status" labels will show if this is ready to be worked on, blocked, or in progress. 21 | - "Need" labels will indicate if additional input or analysis is required. 22 | 23 | Finally, remember to use https://discuss.ipfs.io if you just need general 24 | support. 25 | 26 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 27 | # Comment to be posted to on PRs from first time contributors in your repository 28 | newPRWelcomeComment: > 29 | Thank you for submitting this PR! 30 | 31 | A maintainer will be here shortly to review it. 32 | 33 | We are super grateful, but we are also overloaded! Help us by making sure 34 | that: 35 | 36 | * The context for this PR is clear, with relevant discussion, decisions 37 | and stakeholders linked/mentioned. 38 | 39 | * Your contribution itself is clear (code comments, self-review for the 40 | rest) and in its best form. Follow the [code contribution 41 | guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) 42 | if they apply. 43 | 44 | Getting other community members to do a review would be great help too on 45 | complex PRs (you can ask in the chats/forums). If you are unsure about 46 | something, just leave us a comment. 47 | 48 | Next steps: 49 | 50 | * A maintainer will triage and assign priority to this PR, commenting on 51 | any missing things and potentially assigning a reviewer for high 52 | priority items. 53 | 54 | * The PR gets reviews, discussed and approvals as needed. 55 | 56 | * The PR is merged by maintainers when it has been approved and comments addressed. 57 | 58 | We currently aim to provide initial feedback/triaging within **two business 59 | days**. Please keep an eye on any labelling actions, as these will indicate 60 | priorities and status of your contribution. 61 | 62 | We are very grateful for your contribution! 63 | 64 | 65 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 66 | # Comment to be posted to on pull requests merged by a first time user 67 | # Currently disabled 68 | #firstPRMergeComment: "" 69 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish-github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | jobs: 16 | build-and-publish: 17 | concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 🛎️ 21 | uses: actions/checkout@v4 22 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 23 | run: | 24 | npm install 25 | npm run build 26 | - name: Setup Pages 27 | uses: actions/configure-pages@v5 28 | - name: Upload artifact 29 | uses: actions/upload-pages-artifact@v3 30 | with: 31 | # Upload entire repository 32 | path: 'dist' 33 | - name: Deploy to GitHub Pages 34 | id: deployment 35 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/gateway-conformance.yml: -------------------------------------------------------------------------------- 1 | name: Gateway Conformance Dashboard 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | # Enable this when we're ready to generate the dashboard 9 | # schedule: 10 | # - cron: "0 */6 * * *" # every six hours 11 | 12 | concurrency: 13 | group: "conformance" 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | 20 | jobs: 21 | configure: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | gateways: ${{ steps.set-matrix.outputs.gateways }} 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | - name: Set matrix data 29 | id: set-matrix 30 | run: | 31 | # See details in https://github.com/ipfs/public-gateway-checker/pull/450#discussion_r1318704756 32 | jq . --compact-output gateways.json | \ 33 | xargs --null --max-chars=2000000 -I {} echo "gateways={}" | \ 34 | tee -a "$GITHUB_OUTPUT" 35 | conformance: 36 | runs-on: ubuntu-latest 37 | needs: configure 38 | strategy: 39 | matrix: 40 | gateway_url: ${{ fromJson(needs.configure.outputs.gateways) }} 41 | fail-fast: false 42 | steps: 43 | # 1. Generate the slug used for reporting 44 | - name: Generate slug 45 | id: slug 46 | env: 47 | GATEWAY_URL: ${{ matrix.gateway_url }} 48 | run: | 49 | slug=$(echo "${GATEWAY_URL}" | 50 | sed -e 's/http[s]\?:\/\///' \ 51 | -e 's/[:/@.]/-/g' \ 52 | -e 's/[^A-Za-z0-9\-]/-/g' | 53 | tr "[:upper:]" "[:lower:]") 54 | echo "slug=$slug" >> $GITHUB_OUTPUT 55 | 56 | # 2. Run the gateway-conformance tests 57 | - name: Run gateway-conformance tests 58 | uses: ipfs/gateway-conformance/.github/actions/test@v0 59 | with: 60 | gateway-url: ${{ matrix.gateway_url }} 61 | json: output.json 62 | xml: output.xml 63 | html: output.html 64 | markdown: output.md 65 | report: report.json 66 | accept-test-failure: true 67 | 68 | # 3. Upload the results 69 | - name: Upload MD summary 70 | # TODO: generate a minimal output.md in the action 71 | # See https://github.com/ipfs/gateway-conformance/issues/171 72 | run: cat output.md | sed '/Failures\/Errors/,$d' >> $GITHUB_STEP_SUMMARY 73 | - name: Upload JSON output 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: conformance-${{ steps.slug.outputs.slug }}.json 77 | path: | 78 | ./output.json 79 | ./output.html 80 | ./report.json 81 | - name: Upload HTML report 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: conformance-${{ steps.slug.outputs.slug }}.html 85 | path: output.html 86 | 87 | aggregate: 88 | permissions: 89 | contents: write 90 | runs-on: "ubuntu-latest" 91 | needs: [conformance] 92 | defaults: 93 | run: 94 | shell: bash 95 | steps: 96 | - uses: actions/checkout@v4 97 | - name: Download Artifacts 98 | uses: actions/download-artifact@v4 99 | with: 100 | path: artifacts 101 | - name: Aggregate reports 102 | run: | 103 | mkdir ./reports 104 | 105 | # download-artifact downloads artifacts in a directory named after the artifact 106 | # details: https://github.com/actions/download-artifact#download-all-artifacts 107 | for folder in ./artifacts/conformance-*.json; do 108 | file="${folder}/report.json" 109 | new_name="${folder#.\/artifacts\/conformance-}" # drop path prefix "./artifacts/conformance-" 110 | new_file="./reports/${new_name}" 111 | cp "${file}" "${new_file}" 112 | done 113 | - name: Upload Data Aggregates 114 | # This will be useful for local debugging 115 | if: (failure() || success()) 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: dashboard-reports 119 | path: ./reports 120 | - name: Generate final report 121 | run: | 122 | npm ci --include=dev 123 | npx ts-node ./.github/aggregate.ts ./reports/*.json > ./src/report.json 124 | - name: Upload Report 125 | # This will be useful for local debugging 126 | if: (failure() || success()) 127 | uses: actions/upload-artifact@v4 128 | with: 129 | name: dashboard-report 130 | path: ./src/report.json 131 | - name: Configure git 132 | run: | 133 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com>" 134 | git config --global user.name "${GITHUB_ACTOR}" 135 | - name: Push 136 | run: | 137 | git pull 138 | git add src/report.json 139 | git commit -m "chore: update conformance results" 140 | git push 141 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | name: test & maybe release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | packages: write 14 | pull-requests: write 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | js-test-and-release: 22 | uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v1.0 23 | secrets: 24 | DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} 25 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | UCI_GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN }} 28 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,windows,linux,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,windows,linux,node 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | .pnpm-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # Snowpack dependency directory (https://snowpack.dev/) 95 | web_modules/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Optional stylelint cache 107 | .stylelintcache 108 | 109 | # Microbundle cache 110 | .rpt2_cache/ 111 | .rts2_cache_cjs/ 112 | .rts2_cache_es/ 113 | .rts2_cache_umd/ 114 | 115 | # Optional REPL history 116 | .node_repl_history 117 | 118 | # Output of 'npm pack' 119 | *.tgz 120 | 121 | # Yarn Integrity file 122 | .yarn-integrity 123 | 124 | # dotenv environment variable files 125 | .env 126 | .env.development.local 127 | .env.test.local 128 | .env.production.local 129 | .env.local 130 | 131 | # parcel-bundler cache (https://parceljs.org/) 132 | .cache 133 | .parcel-cache 134 | 135 | # Next.js build output 136 | .next 137 | out 138 | 139 | # Nuxt.js build / generate output 140 | .nuxt 141 | dist 142 | 143 | # Gatsby files 144 | .cache/ 145 | # Comment in the public line in if your project uses Gatsby and not Next.js 146 | # https://nextjs.org/blog/next-9-1#public-directory-support 147 | # public 148 | 149 | # vuepress build output 150 | .vuepress/dist 151 | 152 | # vuepress v2.x temp and cache directory 153 | .temp 154 | 155 | # Docusaurus cache and generated files 156 | .docusaurus 157 | 158 | # Serverless directories 159 | .serverless/ 160 | 161 | # FuseBox cache 162 | .fusebox/ 163 | 164 | # DynamoDB Local files 165 | .dynamodb/ 166 | 167 | # TernJS port file 168 | .tern-port 169 | 170 | # Stores VSCode versions used for testing VSCode extensions 171 | .vscode-test 172 | 173 | # yarn v2 174 | .yarn/cache 175 | .yarn/unplugged 176 | .yarn/build-state.yml 177 | .yarn/install-state.gz 178 | .pnp.* 179 | 180 | ### Node Patch ### 181 | # Serverless Webpack directories 182 | .webpack/ 183 | 184 | # Optional stylelint cache 185 | 186 | # SvelteKit build / generate output 187 | .svelte-kit 188 | 189 | ### VisualStudioCode ### 190 | .vscode/* 191 | !.vscode/settings.json 192 | !.vscode/tasks.json 193 | !.vscode/launch.json 194 | !.vscode/extensions.json 195 | !.vscode/*.code-snippets 196 | 197 | # Local History for Visual Studio Code 198 | .history/ 199 | 200 | # Built Visual Studio Code Extensions 201 | *.vsix 202 | 203 | ### VisualStudioCode Patch ### 204 | # Ignore all local history of files 205 | .history 206 | .ionide 207 | 208 | # Support for Project snippet scope 209 | 210 | ### Windows ### 211 | # Windows thumbnail cache files 212 | Thumbs.db 213 | Thumbs.db:encryptable 214 | ehthumbs.db 215 | ehthumbs_vista.db 216 | 217 | # Dump file 218 | *.stackdump 219 | 220 | # Folder config file 221 | [Dd]esktop.ini 222 | 223 | # Recycle Bin used on file shares 224 | $RECYCLE.BIN/ 225 | 226 | # Windows Installer files 227 | *.cab 228 | *.msi 229 | *.msix 230 | *.msm 231 | *.msp 232 | 233 | # Windows shortcuts 234 | *.lnk 235 | 236 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,windows,linux,node 237 | .envrc 238 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "typescript.format.semicolons": "remove", 4 | "javascript.format.semicolons": "remove", 5 | "typescript.suggest.includeAutomaticOptionalChainCompletions": true, 6 | "editor.formatOnSave": true, 7 | "eslint.codeActionsOnSave.rules": [], 8 | "files.exclude": { 9 | "package-lock.json": true 10 | }, 11 | "standard.enable": true, 12 | "standard.enableGlobally": false, 13 | "standard.run": "onSave", 14 | "standard.autoFixOnSave": true, 15 | "editor.defaultFormatter": "standard.vscode-standard", 16 | "[html]": { 17 | "editor.defaultFormatter": "standard.vscode-standard" 18 | }, 19 | "[json]": { 20 | "editor.defaultFormatter": "standard.vscode-standard" 21 | }, 22 | "[javascript]": { 23 | "editor.defaultFormatter": "standard.vscode-standard" 24 | }, 25 | "files.autoSave": "afterDelay", 26 | "editor.codeActionsOnSave": { 27 | "source.fixAll": "explicit" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.15.2](https://github.com/ipfs/public-gateway-checker/compare/v1.15.1...v1.15.2) (2025-03-24) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * public url ([22b5198](https://github.com/ipfs/public-gateway-checker/commit/22b51985454490b4565ed60427290786975ba0a3)) 7 | 8 | ## [1.15.1](https://github.com/ipfs/public-gateway-checker/compare/v1.15.0...v1.15.1) (2025-02-19) 9 | 10 | 11 | ### Trivial Changes 12 | 13 | * ipfs-geoip v9.2.0 ([#614](https://github.com/ipfs/public-gateway-checker/issues/614)) ([5f1a002](https://github.com/ipfs/public-gateway-checker/commit/5f1a002379430e8c139f50f8809dd618e5927c60)) 14 | 15 | ## [1.15.0](https://github.com/ipfs/public-gateway-checker/compare/v1.14.1...v1.15.0) (2024-09-23) 16 | 17 | 18 | ### Features 19 | 20 | * add flk-ipfs.xyz ([#596](https://github.com/ipfs/public-gateway-checker/issues/596)) ([c8e2f05](https://github.com/ipfs/public-gateway-checker/commit/c8e2f0531502c94e7700bec74d8492f14d05e108)) 21 | 22 | ## [1.14.1](https://github.com/ipfs/public-gateway-checker/compare/v1.14.0...v1.14.1) (2024-09-04) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * remove dwt.my.id ([#593](https://github.com/ipfs/public-gateway-checker/issues/593)) ([219dc8e](https://github.com/ipfs/public-gateway-checker/commit/219dc8e5f2d85809c033a148878e8349bac0a1fe)) 28 | 29 | ## [1.14.0](https://github.com/ipfs/public-gateway-checker/compare/v1.13.0...v1.14.0) (2024-08-30) 30 | 31 | 32 | ### Features 33 | 34 | * add dwt.my.id ([#592](https://github.com/ipfs/public-gateway-checker/issues/592)) ([7d00759](https://github.com/ipfs/public-gateway-checker/commit/7d0075937ea517a590dd5e5dd32e2e606503d9e0)) 35 | 36 | ## [1.13.0](https://github.com/ipfs/public-gateway-checker/compare/v1.12.0...v1.13.0) (2024-08-29) 37 | 38 | 39 | ### Features 40 | 41 | * add flk-ipfs.io ([#591](https://github.com/ipfs/public-gateway-checker/issues/591)) ([4cee251](https://github.com/ipfs/public-gateway-checker/commit/4cee2510dff6693fe5e2a0ee9bfc5b17d7a9dc5c)) 42 | 43 | ## [1.12.0](https://github.com/ipfs/public-gateway-checker/compare/v1.11.0...v1.12.0) (2024-08-29) 44 | 45 | 46 | ### Features 47 | 48 | * add ipfs.cyou ([#579](https://github.com/ipfs/public-gateway-checker/issues/579)) ([66cb2f7](https://github.com/ipfs/public-gateway-checker/commit/66cb2f7d8665fe7eb6ad2ae8d487d95752a86d98)) 49 | 50 | ## [1.11.0](https://github.com/ipfs/public-gateway-checker/compare/v1.10.9...v1.11.0) (2024-08-29) 51 | 52 | 53 | ### Features 54 | 55 | * dlunar.net ([#582](https://github.com/ipfs/public-gateway-checker/issues/582)) ([c00d5a7](https://github.com/ipfs/public-gateway-checker/commit/c00d5a7aee03eaa99fdaf5ed7219eda47229d364)) 56 | 57 | ## [1.10.9](https://github.com/ipfs/public-gateway-checker/compare/v1.10.8...v1.10.9) (2024-07-23) 58 | 59 | 60 | ### Trivial Changes 61 | 62 | * **deps-dev:** bump eslint-plugin-compat from 4.2.0 to 6.0.0 ([#584](https://github.com/ipfs/public-gateway-checker/issues/584)) ([bffe137](https://github.com/ipfs/public-gateway-checker/commit/bffe1374fa90a258617a05b3c4ff149386a506c1)) 63 | 64 | ## [1.10.8](https://github.com/ipfs/public-gateway-checker/compare/v1.10.7...v1.10.8) (2024-06-11) 65 | 66 | 67 | ### Trivial Changes 68 | 69 | * Delist CloudFlare's gateways due to their discontinuation. ([#572](https://github.com/ipfs/public-gateway-checker/issues/572)) ([5c46874](https://github.com/ipfs/public-gateway-checker/commit/5c4687488d46f2004e2db409ba0c5876430cc9be)) 70 | * update conformance results ([87935be](https://github.com/ipfs/public-gateway-checker/commit/87935be73b3b81c745866d0dcb899d4e5d8953f2)) 71 | 72 | ## [1.10.7](https://github.com/ipfs/public-gateway-checker/compare/v1.10.6...v1.10.7) (2024-04-22) 73 | 74 | 75 | ### Trivial Changes 76 | 77 | * **deps-dev:** bump the npm_and_yarn group across 1 directories with 2 updates ([#540](https://github.com/ipfs/public-gateway-checker/issues/540)) ([445d849](https://github.com/ipfs/public-gateway-checker/commit/445d849b81909e40a7d68fc0ac2fa9b196c78e87)) 78 | * update conformance results ([6e3108b](https://github.com/ipfs/public-gateway-checker/commit/6e3108b49e0ed6ac94ce454e1e570c9304898747)) 79 | 80 | ## [1.10.6](https://github.com/ipfs/public-gateway-checker/compare/v1.10.5...v1.10.6) (2024-04-18) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * disable countly metrics ([#553](https://github.com/ipfs/public-gateway-checker/issues/553)) ([68733bd](https://github.com/ipfs/public-gateway-checker/commit/68733bdc8fc1157e26be040167213ec5b5e2daf2)) 86 | 87 | 88 | ### Trivial Changes 89 | 90 | * update conformance results ([0f7217d](https://github.com/ipfs/public-gateway-checker/commit/0f7217deb919f3a0202ba712654da8f713c840a6)) 91 | 92 | ## [1.10.5](https://github.com/ipfs/public-gateway-checker/compare/v1.10.4...v1.10.5) (2024-04-18) 93 | 94 | 95 | ### Trivial Changes 96 | 97 | * **deps-dev:** bump ts-node from 10.9.1 to 10.9.2 ([#521](https://github.com/ipfs/public-gateway-checker/issues/521)) ([229d210](https://github.com/ipfs/public-gateway-checker/commit/229d210dfad0b429601241f24bc2d616f1784e01)) 98 | * **deps:** bump actions/configure-pages from 4 to 5 ([#549](https://github.com/ipfs/public-gateway-checker/issues/549)) ([26e7665](https://github.com/ipfs/public-gateway-checker/commit/26e766595a614e10a93e429cfd9f4303fa87dad9)) 99 | 100 | ## [1.10.4](https://github.com/ipfs/public-gateway-checker/compare/v1.10.3...v1.10.4) (2024-04-18) 101 | 102 | 103 | ### Trivial Changes 104 | 105 | * **deps-dev:** bump eslint-config-ipfs from 4.0.3 to 6.0.1 ([#522](https://github.com/ipfs/public-gateway-checker/issues/522)) ([77ce4a9](https://github.com/ipfs/public-gateway-checker/commit/77ce4a917e67d3492d0eb6a444e59c75656eaba5)) 106 | * **deps-dev:** bump js-beautify from 1.14.8 to 1.15.1 ([#537](https://github.com/ipfs/public-gateway-checker/issues/537)) ([533a84f](https://github.com/ipfs/public-gateway-checker/commit/533a84f4687c03b5cdf98d376a8278e79875fa6a)) 107 | 108 | ## [1.10.3](https://github.com/ipfs/public-gateway-checker/compare/v1.10.2...v1.10.3) (2024-04-18) 109 | 110 | 111 | ### Trivial Changes 112 | 113 | * **deps-dev:** bump browserslist from 4.21.10 to 4.23.0 ([#535](https://github.com/ipfs/public-gateway-checker/issues/535)) ([b5d2dd4](https://github.com/ipfs/public-gateway-checker/commit/b5d2dd4f989fa0d04fd271185cd0fe4155a302a4)) 114 | 115 | ## [1.10.2](https://github.com/ipfs/public-gateway-checker/compare/v1.10.1...v1.10.2) (2024-04-15) 116 | 117 | 118 | ### Trivial Changes 119 | 120 | * **deps:** bump actions/download-artifact from 3 to 4 ([#504](https://github.com/ipfs/public-gateway-checker/issues/504)) ([402b232](https://github.com/ipfs/public-gateway-checker/commit/402b2328f884224962da4c4de42735bd4f50cb5d)) 121 | * **deps:** bump actions/upload-artifact from 3 to 4 ([#503](https://github.com/ipfs/public-gateway-checker/issues/503)) ([a6c407e](https://github.com/ipfs/public-gateway-checker/commit/a6c407e1316534f495a05757c90b1297084603c3)) 122 | 123 | ## [1.10.1](https://github.com/ipfs/public-gateway-checker/compare/v1.10.0...v1.10.1) (2024-04-11) 124 | 125 | 126 | ### Trivial Changes 127 | 128 | * **deps-dev:** bump ipfs-geoip from 9.0.1 to 9.1.0 ([#519](https://github.com/ipfs/public-gateway-checker/issues/519)) ([33c71d4](https://github.com/ipfs/public-gateway-checker/commit/33c71d43a02dba9cc501b576cbe8ef64876c3a71)) 129 | 130 | ## [1.10.0](https://github.com/ipfs/public-gateway-checker/compare/v1.9.6...v1.10.0) (2024-04-10) 131 | 132 | 133 | ### Features 134 | 135 | * add storry.tv ([#550](https://github.com/ipfs/public-gateway-checker/issues/550)) ([127e5eb](https://github.com/ipfs/public-gateway-checker/commit/127e5eb6db5ffd72ceabfb603b7b46b7365ad42a)) 136 | 137 | 138 | ### Trivial Changes 139 | 140 | * **deps-dev:** bump typescript from 5.1.6 to 5.4.4 ([#551](https://github.com/ipfs/public-gateway-checker/issues/551)) ([eaa45c1](https://github.com/ipfs/public-gateway-checker/commit/eaa45c1b75480e1b268313fccbcbb9d8afd4c37d)) 141 | * update conformance results ([0723c33](https://github.com/ipfs/public-gateway-checker/commit/0723c335a75ea690e8f49654a604619641ecf55e)) 142 | 143 | ## [1.9.6](https://github.com/ipfs/public-gateway-checker/compare/v1.9.5...v1.9.6) (2024-03-21) 144 | 145 | 146 | ### Trivial Changes 147 | 148 | * update conformance results ([9167eba](https://github.com/ipfs/public-gateway-checker/commit/9167eba28fc8b44c0d684e0aa65789af1e0d0000)) 149 | 150 | ## [1.9.5](https://github.com/ipfs/public-gateway-checker/compare/v1.9.4...v1.9.5) (2024-02-27) 151 | 152 | 153 | ### Trivial Changes 154 | 155 | * update conformance results ([eb73d9f](https://github.com/ipfs/public-gateway-checker/commit/eb73d9fa1ec09269f0d476e704b459d8d56bd49a)) 156 | 157 | ## [1.9.4](https://github.com/ipfs/public-gateway-checker/compare/v1.9.3...v1.9.4) (2024-02-23) 158 | 159 | 160 | ### Trivial Changes 161 | 162 | * remove dweb and fleek ([#541](https://github.com/ipfs/public-gateway-checker/issues/541)) ([08f4a0c](https://github.com/ipfs/public-gateway-checker/commit/08f4a0cc30d3bc67c81f366717a1f6ddfffacd68)) 163 | * update conformance results ([c25d713](https://github.com/ipfs/public-gateway-checker/commit/c25d713b2506359cc7ac593e62fd985eb0ced762)) 164 | 165 | ## [1.9.3](https://github.com/ipfs/public-gateway-checker/compare/v1.9.2...v1.9.3) (2024-02-22) 166 | 167 | 168 | ### Trivial Changes 169 | 170 | * update conformance results ([44b8c21](https://github.com/ipfs/public-gateway-checker/commit/44b8c21023ba8614d77089a2f8da296a1810373b)) 171 | * Update Fleek url ([#538](https://github.com/ipfs/public-gateway-checker/issues/538)) ([24a5887](https://github.com/ipfs/public-gateway-checker/commit/24a58874806fbe55750d9866f726122cd6d0ae18)) 172 | 173 | ## [1.9.2](https://github.com/ipfs/public-gateway-checker/compare/v1.9.1...v1.9.2) (2024-02-22) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * remove unresolvable gateways ([#532](https://github.com/ipfs/public-gateway-checker/issues/532)) ([ee4d247](https://github.com/ipfs/public-gateway-checker/commit/ee4d247dfc87fb2c252e5a041bc82b2d7edd2465)) 179 | 180 | 181 | ### Trivial Changes 182 | 183 | * **deps-dev:** bump aegir from 42.2.1 to 42.2.3 ([#526](https://github.com/ipfs/public-gateway-checker/issues/526)) ([20139d1](https://github.com/ipfs/public-gateway-checker/commit/20139d19cfd8540e1985100ef9e10a192aea397e)) 184 | * remove non recursive gateways ([da1ea97](https://github.com/ipfs/public-gateway-checker/commit/da1ea97c00a14d69875d2762b02255413b87417d)) 185 | * Update .github/workflows/stale.yml [skip ci] ([622bd88](https://github.com/ipfs/public-gateway-checker/commit/622bd883fc69ea159e88b28dd962ead9244a7716)) 186 | * update conformance results ([bca195f](https://github.com/ipfs/public-gateway-checker/commit/bca195fce7c3d13673a99085ec7fb76b0d58d408)) 187 | * update conformance results ([54a2fb9](https://github.com/ipfs/public-gateway-checker/commit/54a2fb9910eda712a4e279a125d31ba2507d864a)) 188 | * update conformance results ([621a352](https://github.com/ipfs/public-gateway-checker/commit/621a352d891f2ba7a2db31552062cb138a328acf)) 189 | * update conformance results ([29a2aee](https://github.com/ipfs/public-gateway-checker/commit/29a2aeea8ebf2e397e2933c0a6ae7a42efb1c4f2)) 190 | * update conformance results ([2e06a78](https://github.com/ipfs/public-gateway-checker/commit/2e06a786bcc3712befb22de0abe9ee36f4181086)) 191 | 192 | ## [1.9.1](https://github.com/ipfs/public-gateway-checker/compare/v1.9.0...v1.9.1) (2024-02-09) 193 | 194 | 195 | ### Trivial Changes 196 | 197 | * update conformance results ([2d5434c](https://github.com/ipfs/public-gateway-checker/commit/2d5434c8278a341162add67cf9d26caa69c09c16)) 198 | 199 | ## [1.9.0](https://github.com/ipfs/public-gateway-checker/compare/v1.8.6...v1.9.0) (2024-02-07) 200 | 201 | 202 | ### Features 203 | 204 | * add trustless-gateway.link ([#527](https://github.com/ipfs/public-gateway-checker/issues/527)) ([d217c73](https://github.com/ipfs/public-gateway-checker/commit/d217c73f8bd98c2286ed9a4a9005dd5cb26ebca1)) 205 | 206 | 207 | ### Bug Fixes 208 | 209 | * gateway list ([#528](https://github.com/ipfs/public-gateway-checker/issues/528)) ([740e582](https://github.com/ipfs/public-gateway-checker/commit/740e58222fcfdc18c8168daa2e74ef7dfae70a88)) 210 | 211 | 212 | ### Trivial Changes 213 | 214 | * Update .github/dependabot.yml [skip ci] ([eae6603](https://github.com/ipfs/public-gateway-checker/commit/eae6603cb6d887245e47611eded6159e95e2b6e0)) 215 | * Update .github/workflows/stale.yml [skip ci] ([bf0c1e5](https://github.com/ipfs/public-gateway-checker/commit/bf0c1e5a3b9b20785ad92a60471804c3038e2b5f)) 216 | 217 | ## [1.8.6](https://github.com/ipfs/public-gateway-checker/compare/v1.8.5...v1.8.6) (2024-01-17) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * 🗑️ Removing konubinix.eu ([#492](https://github.com/ipfs/public-gateway-checker/issues/492)) ([0b28d72](https://github.com/ipfs/public-gateway-checker/commit/0b28d72421f24129a8883bc79e0b662212dc7b79)) 223 | 224 | 225 | ### Trivial Changes 226 | 227 | * **deps-dev:** bump aegir from 41.1.8 to 42.2.1 ([#517](https://github.com/ipfs/public-gateway-checker/issues/517)) ([2fb3445](https://github.com/ipfs/public-gateway-checker/commit/2fb3445c78cc80b5d70b77c956bb0d7e3a9947ab)) 228 | * update conformance results ([44d369f](https://github.com/ipfs/public-gateway-checker/commit/44d369f938f9153b15e8c2f024257f756247162e)) 229 | 230 | ## [1.8.5](https://github.com/ipfs/public-gateway-checker/compare/v1.8.4...v1.8.5) (2023-12-21) 231 | 232 | 233 | ### Trivial Changes 234 | 235 | * **gateway:** 🗑️ Removing cthd.icu ([#502](https://github.com/ipfs/public-gateway-checker/issues/502)) ([a78066b](https://github.com/ipfs/public-gateway-checker/commit/a78066b22f1a31f0cca7ce78b80962414dbee8d2)) 236 | 237 | ## [1.8.4](https://github.com/ipfs/public-gateway-checker/compare/v1.8.3...v1.8.4) (2023-11-22) 238 | 239 | 240 | ### Trivial Changes 241 | 242 | * remove ninetailed.ninja ([#490](https://github.com/ipfs/public-gateway-checker/issues/490)) ([2945831](https://github.com/ipfs/public-gateway-checker/commit/2945831a7b8e9e1c43fa64944c3485af8eac719d)) 243 | 244 | ## [1.8.3](https://github.com/ipfs/public-gateway-checker/compare/v1.8.2...v1.8.3) (2023-11-03) 245 | 246 | 247 | ### Trivial Changes 248 | 249 | * **deps-dev:** bump aegir from 40.0.13 to 41.1.8 ([#482](https://github.com/ipfs/public-gateway-checker/issues/482)) ([6aab923](https://github.com/ipfs/public-gateway-checker/commit/6aab92304ef1106d50984d93161f2a09b94f3ae8)) 250 | 251 | ## [1.8.2](https://github.com/ipfs/public-gateway-checker/compare/v1.8.1...v1.8.2) (2023-10-27) 252 | 253 | 254 | ### Trivial Changes 255 | 256 | * **deps-dev:** bump eslint-plugin-compat from 4.1.4 to 4.2.0 ([#454](https://github.com/ipfs/public-gateway-checker/issues/454)) ([5d185ee](https://github.com/ipfs/public-gateway-checker/commit/5d185eeedf96e11ba67473bcf4707119f5cef2c8)) 257 | * **deps:** bump actions/checkout from 3 to 4 ([#457](https://github.com/ipfs/public-gateway-checker/issues/457)) ([177a90e](https://github.com/ipfs/public-gateway-checker/commit/177a90e54021542af7073faa360f8e6762c18270)) 258 | 259 | ## [1.8.1](https://github.com/ipfs/public-gateway-checker/compare/v1.8.0...v1.8.1) (2023-10-20) 260 | 261 | 262 | ### Trivial Changes 263 | 264 | * update conformance results ([0ad4003](https://github.com/ipfs/public-gateway-checker/commit/0ad4003c2a8d1ccc0ac86a416362c60b6e6b61fe)) 265 | 266 | ## [1.8.0](https://github.com/ipfs/public-gateway-checker/compare/v1.7.0...v1.8.0) (2023-09-29) 267 | 268 | 269 | ### Features 270 | 271 | * generate results with gateway-conformance ([#450](https://github.com/ipfs/public-gateway-checker/issues/450)) ([e200e3a](https://github.com/ipfs/public-gateway-checker/commit/e200e3a703ae5489716312e9a134740a032e7327)), closes [#442](https://github.com/ipfs/public-gateway-checker/issues/442) 272 | 273 | ## [1.7.0](https://github.com/ipfs/public-gateway-checker/compare/v1.6.1...v1.7.0) (2023-09-29) 274 | 275 | 276 | ### Features 277 | 278 | * twdragon.net ([#444](https://github.com/ipfs/public-gateway-checker/issues/444)) ([3a71191](https://github.com/ipfs/public-gateway-checker/commit/3a71191ff8ea6ba75db016dd2d77f4c3def7e400)) 279 | 280 | ## [1.6.1](https://github.com/ipfs/public-gateway-checker/compare/v1.6.0...v1.6.1) (2023-08-07) 281 | 282 | 283 | ### Bug Fixes 284 | 285 | * **gateways:** Removing Offending Gateways ([#443](https://github.com/ipfs/public-gateway-checker/issues/443)) ([7bf97f5](https://github.com/ipfs/public-gateway-checker/commit/7bf97f53b376f66a1123ba412925413c94aa5859)) 286 | 287 | ## [1.6.0](https://github.com/ipfs/public-gateway-checker/compare/v1.5.5...v1.6.0) (2023-07-28) 288 | 289 | 290 | ### Features 291 | 292 | * add storage.web3ph.dev ([#435](https://github.com/ipfs/public-gateway-checker/issues/435)) ([90202b1](https://github.com/ipfs/public-gateway-checker/commit/90202b1db668338d0e6bbf27e65a028779528647)) 293 | 294 | ## [1.5.5](https://github.com/ipfs/public-gateway-checker/compare/v1.5.4...v1.5.5) (2023-07-28) 295 | 296 | 297 | ### Bug Fixes 298 | 299 | * remove red-page ipns.co ([#434](https://github.com/ipfs/public-gateway-checker/issues/434)) ([dd30dfc](https://github.com/ipfs/public-gateway-checker/commit/dd30dfc62633db06560e27ebc27bca395e7ff3f9)) 300 | 301 | ## [1.5.4](https://github.com/ipfs/public-gateway-checker/compare/v1.5.3...v1.5.4) (2023-07-13) 302 | 303 | 304 | ### Bug Fixes 305 | 306 | * remove ipfs and ipfs-http-client deps ([#429](https://github.com/ipfs/public-gateway-checker/issues/429)) ([7618207](https://github.com/ipfs/public-gateway-checker/commit/7618207cb806b3106be7791a94ee128415769c7e)) 307 | 308 | ## [1.5.3](https://github.com/ipfs/public-gateway-checker/compare/v1.5.2...v1.5.3) (2023-07-13) 309 | 310 | 311 | ### Trivial Changes 312 | 313 | * **deps:** bump @grpc/grpc-js from 1.7.3 to 1.8.18 ([#428](https://github.com/ipfs/public-gateway-checker/issues/428)) ([0fdf876](https://github.com/ipfs/public-gateway-checker/commit/0fdf876a049b660410a6477aeec6d5328856c4cd)) 314 | 315 | ## [1.5.2](https://github.com/ipfs/public-gateway-checker/compare/v1.5.1...v1.5.2) (2023-07-13) 316 | 317 | 318 | ### Trivial Changes 319 | 320 | * **deps:** update deps and npm audit fix ([#427](https://github.com/ipfs/public-gateway-checker/issues/427)) ([08b508e](https://github.com/ipfs/public-gateway-checker/commit/08b508ec8e591417214f73d7689f5094e3f6f9db)) 321 | * update pull request template ([1c06dbc](https://github.com/ipfs/public-gateway-checker/commit/1c06dbc2eb5d891c9bae4c8c64e83e63d61fdf1b)) 322 | 323 | ## [1.5.1](https://github.com/ipfs/public-gateway-checker/compare/v1.5.0...v1.5.1) (2023-07-13) 324 | 325 | 326 | ### Bug Fixes 327 | 328 | * reduce PR hell ([#425](https://github.com/ipfs/public-gateway-checker/issues/425)) ([64dce8d](https://github.com/ipfs/public-gateway-checker/commit/64dce8d23d6b1186c0c3e899eccf95ef640c118c)), closes [#419](https://github.com/ipfs/public-gateway-checker/issues/419) 329 | 330 | ## [1.5.0](https://github.com/ipfs/public-gateway-checker/compare/v1.4.0...v1.5.0) (2023-07-13) 331 | 332 | 333 | ### Features 334 | 335 | * add getipfs.com ([#419](https://github.com/ipfs/public-gateway-checker/issues/419)) ([f356d06](https://github.com/ipfs/public-gateway-checker/commit/f356d06f8b74035a5d3fcb21c61c76ad4aa2efa5)) 336 | 337 | ## [1.4.0](https://github.com/ipfs/public-gateway-checker/compare/v1.3.2...v1.4.0) (2023-07-13) 338 | 339 | 340 | ### Features 341 | 342 | * add twdragon.net ([#361](https://github.com/ipfs/public-gateway-checker/issues/361)) ([16454d3](https://github.com/ipfs/public-gateway-checker/commit/16454d34b57c67aee525d926e51a31f8e88e5a2b)) 343 | 344 | 345 | ### Trivial Changes 346 | 347 | * Update .github/dependabot.yml [skip ci] ([035d92c](https://github.com/ipfs/public-gateway-checker/commit/035d92cec96f73fb37b4b32f9a97427cdfdcef6f)) 348 | * Update .github/workflows/stale.yml [skip ci] ([cdd2e14](https://github.com/ipfs/public-gateway-checker/commit/cdd2e14240bf2f00268cce830120d813996456ce)) 349 | 350 | ## [1.3.2](https://github.com/ipfs/public-gateway-checker/compare/v1.3.1...v1.3.2) (2023-06-15) 351 | 352 | 353 | ### Trivial Changes 354 | 355 | * **deps:** bump s0/git-publish-subdir-action from 399aab378450f99b7de6767f62b0d1dbfcb58b53 to 92faf786f11dfa44fc366ac3eb274d193ca1af7e ([#417](https://github.com/ipfs/public-gateway-checker/issues/417)) ([041f23e](https://github.com/ipfs/public-gateway-checker/commit/041f23eeda151c252e46ba17e61082e4aac4345c)) 356 | 357 | ## [1.3.1](https://github.com/ipfs/public-gateway-checker/compare/v1.3.0...v1.3.1) (2023-06-15) 358 | 359 | 360 | ### Trivial Changes 361 | 362 | * **deps:** bump actions/checkout from 2 to 3 ([#416](https://github.com/ipfs/public-gateway-checker/issues/416)) ([0706590](https://github.com/ipfs/public-gateway-checker/commit/07065904417fa2ee609b697b54fb7feb156b13c2)) 363 | * **deps:** bump codecov/codecov-action from 3.1.1 to 3.1.4 ([#415](https://github.com/ipfs/public-gateway-checker/issues/415)) ([2caf047](https://github.com/ipfs/public-gateway-checker/commit/2caf0474a5d8bc3ccfe3b6ab95b37b0933fcd5f5)) 364 | 365 | ## [1.3.0](https://github.com/ipfs/public-gateway-checker/compare/v1.2.2...v1.3.0) (2023-06-14) 366 | 367 | 368 | ### Features 369 | 370 | * use typescript ([#194](https://github.com/ipfs/public-gateway-checker/issues/194)) ([9bbe1b5](https://github.com/ipfs/public-gateway-checker/commit/9bbe1b53a18215efa3304def3b3ac7f3e8495caf)) 371 | 372 | 373 | ### Trivial Changes 374 | 375 | * Update .github/workflows/stale.yml [skip ci] ([25aecd9](https://github.com/ipfs/public-gateway-checker/commit/25aecd9f865b6d2eb612006070826b038fb9dfbe)) 376 | 377 | ## [1.2.2](https://github.com/ipfs/public-gateway-checker/compare/v1.2.1...v1.2.2) (2023-06-08) 378 | 379 | 380 | ### Trivial Changes 381 | 382 | * update eslint-config-ipfs and fix lint ([#413](https://github.com/ipfs/public-gateway-checker/issues/413)) ([165f35c](https://github.com/ipfs/public-gateway-checker/commit/165f35ce394f0f6e452174e0133cd111ba48170e)) 383 | 384 | ## [1.2.1](https://github.com/ipfs/public-gateway-checker/compare/v1.2.0...v1.2.1) (2023-06-08) 385 | 386 | 387 | ### Trivial Changes 388 | 389 | * **deps:** bump undici from 5.10.0 to 5.20.0 ([#387](https://github.com/ipfs/public-gateway-checker/issues/387)) ([5157670](https://github.com/ipfs/public-gateway-checker/commit/5157670558880761309ff16a84c12d04a44381cc)) 390 | 391 | ## [1.2.0](https://github.com/ipfs/public-gateway-checker/compare/v1.1.0...v1.2.0) (2023-05-26) 392 | 393 | 394 | ### Features 395 | 396 | * rename starbase.gw3.io to gw3.io ([#405](https://github.com/ipfs/public-gateway-checker/issues/405)) ([f1f8522](https://github.com/ipfs/public-gateway-checker/commit/f1f8522c52e97bf53e9f98060fc7b2eb0f792909)) 397 | 398 | ## [1.1.0](https://github.com/ipfs/public-gateway-checker/compare/v1.0.3...v1.1.0) (2023-05-14) 399 | 400 | 401 | ### Features 402 | 403 | * add starbase.gw3.io ([#398](https://github.com/ipfs/public-gateway-checker/issues/398)) ([822d885](https://github.com/ipfs/public-gateway-checker/commit/822d8853d809bfaef3b9d5cef05cb207db7eb0b8)) 404 | 405 | ## [1.0.3](https://github.com/ipfs/public-gateway-checker/compare/v1.0.2...v1.0.3) (2023-05-12) 406 | 407 | 408 | ### Trivial Changes 409 | 410 | * **deps:** bump xml2js and @achingbrain/nat-port-mapper ([#401](https://github.com/ipfs/public-gateway-checker/issues/401)) ([a5ff08b](https://github.com/ipfs/public-gateway-checker/commit/a5ff08b31611907e3671ae7ca98e0405023c570a)) 411 | 412 | ## [1.0.2](https://github.com/ipfs/public-gateway-checker/compare/v1.0.1...v1.0.2) (2023-05-12) 413 | 414 | 415 | ### Trivial Changes 416 | 417 | * **deps-dev:** bump aegir from 37.5.6 to 38.1.7 ([#388](https://github.com/ipfs/public-gateway-checker/issues/388)) ([2318df6](https://github.com/ipfs/public-gateway-checker/commit/2318df6348e51e77ce44a5ecc196ebea979640b2)) 418 | * **deps:** bump @sideway/formula from 3.0.0 to 3.0.1 ([#380](https://github.com/ipfs/public-gateway-checker/issues/380)) ([2e4d663](https://github.com/ipfs/public-gateway-checker/commit/2e4d66312205af463801d58a855b8bf3f34a8fce)) 419 | 420 | ## [1.0.1](https://github.com/ipfs/public-gateway-checker/compare/v1.0.0...v1.0.1) (2023-04-17) 421 | 422 | 423 | ### Trivial Changes 424 | 425 | * npx update-browserslist-db@latest ([194e968](https://github.com/ipfs/public-gateway-checker/commit/194e96883837e03c1355deef409a3447b2330e18)) 426 | 427 | ## 1.0.0 (2023-04-17) 428 | 429 | 430 | ### Features 431 | 432 | * /ipns/ check ([#313](https://github.com/ipfs/public-gateway-checker/issues/313)) ([10a5c13](https://github.com/ipfs/public-gateway-checker/commit/10a5c13b1e49a4df8bf1711458fe0a7c33b900ba)) 433 | * add countly metrics ([#309](https://github.com/ipfs/public-gateway-checker/issues/309)) ([c727202](https://github.com/ipfs/public-gateway-checker/commit/c727202d5a98b9e191a9d979fdb4e40e8d23cf43)) 434 | * add cthd.icu ([#294](https://github.com/ipfs/public-gateway-checker/issues/294)) ([a2e0102](https://github.com/ipfs/public-gateway-checker/commit/a2e01026c015d1f93fdbfe8058027fb440432dec)) 435 | * add https://ipfs.czip.it ([#374](https://github.com/ipfs/public-gateway-checker/issues/374)) ([f3dde51](https://github.com/ipfs/public-gateway-checker/commit/f3dde51902836fa2f259cd3e6ae9913fc68bb8c7)) 436 | * add https://ipfs.joaoleitao.org ([#323](https://github.com/ipfs/public-gateway-checker/issues/323)) ([787f131](https://github.com/ipfs/public-gateway-checker/commit/787f131e2f1c712d5a558eb873c7d445bd4bfd06)) 437 | * add ipfs.1-2.dev ([#169](https://github.com/ipfs/public-gateway-checker/issues/169)) ([c764ad6](https://github.com/ipfs/public-gateway-checker/commit/c764ad6d4a137e103a43c2f3572cca201e3dfc73)) 438 | * add ipfs.drink.cafe ([#116](https://github.com/ipfs/public-gateway-checker/issues/116)) ([cc84899](https://github.com/ipfs/public-gateway-checker/commit/cc8489908f0f5154df051f13b2d94e00f9f1ed38)) 439 | * add ipfs.jpu.jp ([#348](https://github.com/ipfs/public-gateway-checker/issues/348)) ([b52b8c7](https://github.com/ipfs/public-gateway-checker/commit/b52b8c79ee15361cb27e0dd11647b3af6d8e0a25)) 440 | * add ipfs.litnet.work ([#222](https://github.com/ipfs/public-gateway-checker/issues/222)) ([1fd2e68](https://github.com/ipfs/public-gateway-checker/commit/1fd2e68c6752d511271944df9f945f44b717518a)) 441 | * add ipfs.pinksheep.whizzzkid.dev ([#326](https://github.com/ipfs/public-gateway-checker/issues/326)) ([d40a2c1](https://github.com/ipfs/public-gateway-checker/commit/d40a2c153bc0a2cbe4c7a1773e1484d326ba1cd4)) 442 | * add ipfs.soul-network.com ([#389](https://github.com/ipfs/public-gateway-checker/issues/389)) ([57fe04d](https://github.com/ipfs/public-gateway-checker/commit/57fe04db01bc4661bb1f19a2782f40c1d0924fff)) 443 | * add nftstorage.link gateway ([#204](https://github.com/ipfs/public-gateway-checker/issues/204)) ([e588108](https://github.com/ipfs/public-gateway-checker/commit/e58810802f373f1a7e8548fa231be1e159eab084)) 444 | * add Onion Gateway (TOR) fzdqwfb5ml56oadins5jpuhe6ki6bk33umri35p5kt2tue4fpws5efid.onion ([#212](https://github.com/ipfs/public-gateway-checker/issues/212)) ([01ff12f](https://github.com/ipfs/public-gateway-checker/commit/01ff12f05bd66763e4d3470d5bc9742bff3add97)) 445 | * add w3s.link gateway ([#288](https://github.com/ipfs/public-gateway-checker/issues/288)) ([000a26f](https://github.com/ipfs/public-gateway-checker/commit/000a26fd2da3bb12e18f46939136d43029a4a67c)) 446 | * country flags ([#96](https://github.com/ipfs/public-gateway-checker/issues/96)) ([84a31fe](https://github.com/ipfs/public-gateway-checker/commit/84a31fe2eb8ce92167ff658b973940d623f465cf)) 447 | * Create CODEOWNERS ([#283](https://github.com/ipfs/public-gateway-checker/issues/283)) ([b62b41c](https://github.com/ipfs/public-gateway-checker/commit/b62b41ce048a3db79b2dabed7e09e08f9f3fe2d7)) 448 | * Deleted https://ipfs.czip.it ([#393](https://github.com/ipfs/public-gateway-checker/issues/393)) ([77d67a4](https://github.com/ipfs/public-gateway-checker/commit/77d67a490c4056d9e5113353c069d8e5c78b52e3)) 449 | * Implementing Trustless Server Checks ([#310](https://github.com/ipfs/public-gateway-checker/issues/310)) ([4a2c926](https://github.com/ipfs/public-gateway-checker/commit/4a2c926774efc60b699d186059c49a866d4c7c41)) 450 | * improved Origin detection via img tag ([#117](https://github.com/ipfs/public-gateway-checker/issues/117)) ([8407e80](https://github.com/ipfs/public-gateway-checker/commit/8407e809d19a52a944f28523ec6413d1bbd52506)) 451 | * improved origin isolation check ([#148](https://github.com/ipfs/public-gateway-checker/issues/148)) ([abd4c1c](https://github.com/ipfs/public-gateway-checker/commit/abd4c1ced572a041c0f8111d841bd4652a392bc6)) 452 | * Introducing Service Worker For Cache Busting ([#357](https://github.com/ipfs/public-gateway-checker/issues/357)) ([0536782](https://github.com/ipfs/public-gateway-checker/commit/0536782f031497b564bdada2c4c9225a161147c7)) 453 | * new gateway https://ipfs.tayfundogdas.me/ipfs ([#321](https://github.com/ipfs/public-gateway-checker/issues/321)) ([9d5b552](https://github.com/ipfs/public-gateway-checker/commit/9d5b55258aa3a508950e8d030d9635f9dfd38da5)) 454 | * remove ipfs.foxgirl.dev ([#155](https://github.com/ipfs/public-gateway-checker/issues/155)) ([15fd028](https://github.com/ipfs/public-gateway-checker/commit/15fd028daca252a0d4c136bf144e1a59f92661b2)) 455 | * remove smartsignature.io ([#146](https://github.com/ipfs/public-gateway-checker/issues/146)) ([77b45b6](https://github.com/ipfs/public-gateway-checker/commit/77b45b6039a3e1718a0546cdcbf163a3744e4c8b)) 456 | * subdomain gateways and Origin isolation check ([#78](https://github.com/ipfs/public-gateway-checker/issues/78)) ([afcbffa](https://github.com/ipfs/public-gateway-checker/commit/afcbffa2d47626fcd7dac04059f3c266b86969a2)) 457 | * update geoip dataset (2020-10-13) ([4187738](https://github.com/ipfs/public-gateway-checker/commit/41877381363ff802a9cfb5724973b06e6fe735ea)) 458 | * update geoip dataset (2020-10-13) ([#115](https://github.com/ipfs/public-gateway-checker/issues/115)) ([782b66b](https://github.com/ipfs/public-gateway-checker/commit/782b66b43de07d85b95529a42973165541c18f6c)) 459 | * use typescript ([#194](https://github.com/ipfs/public-gateway-checker/issues/194)) ([10958e6](https://github.com/ipfs/public-gateway-checker/commit/10958e617627315abb1e7dfb54e85784b64c279f)) 460 | 461 | 462 | ### Bug Fixes 463 | 464 | * :rewind: Reverting [#323](https://github.com/ipfs/public-gateway-checker/issues/323): ipfs.joaoleitao.org ([#394](https://github.com/ipfs/public-gateway-checker/issues/394)) ([b5bb34c](https://github.com/ipfs/public-gateway-checker/commit/b5bb34c5bc8dc66adaea35cb6592a93b3587a4f9)) 465 | * **ci:** add empty commit to fix lint checks on master ([3ae6aa0](https://github.com/ipfs/public-gateway-checker/commit/3ae6aa007a37f34db8b686aa0bb46fad0bcc6754)) 466 | * **ci:** skip test if no code changed ([#210](https://github.com/ipfs/public-gateway-checker/issues/210)) ([7d6d628](https://github.com/ipfs/public-gateway-checker/commit/7d6d628d1f1ef01535fb58334aa6fe3e47008fac)) 467 | * cleanup entries missing DNS A record ([#180](https://github.com/ipfs/public-gateway-checker/issues/180)) ([2b7ad30](https://github.com/ipfs/public-gateway-checker/commit/2b7ad308c42a1d95b7b86693f4df9ef216d4c789)) 468 | * do not redirect IPNS checks ([#325](https://github.com/ipfs/public-gateway-checker/issues/325)) ([79bb51d](https://github.com/ipfs/public-gateway-checker/commit/79bb51db9fbacdf9298a0172e2817e4203a39e65)) 469 | * flag column and new ipfs-geoip dataset ([#319](https://github.com/ipfs/public-gateway-checker/issues/319)) ([f5fc723](https://github.com/ipfs/public-gateway-checker/commit/f5fc7235e20190472413e2f3557269cfa77f6445)) 470 | * metrics consent prompt location and styling ([#353](https://github.com/ipfs/public-gateway-checker/issues/353)) ([e709f2b](https://github.com/ipfs/public-gateway-checker/commit/e709f2bd3fc7b61fe9324132039a94c5a63b4fdb)) 471 | * npm start should work without prior cmds ([#307](https://github.com/ipfs/public-gateway-checker/issues/307)) ([7ebe2e5](https://github.com/ipfs/public-gateway-checker/commit/7ebe2e519e08b3bbf26ab083a0a81693f347921a)) 472 | * opt-out from redirects done by browser extension ([6dd5f51](https://github.com/ipfs/public-gateway-checker/commit/6dd5f510df6541ba845143b1d0ed5e60caf92a14)) 473 | * origin typo ([#200](https://github.com/ipfs/public-gateway-checker/issues/200)) ([d198abb](https://github.com/ipfs/public-gateway-checker/commit/d198abb5b43dfc49b5c3d9d4ef5939b5bbd8fd44)) 474 | * **origin:** confirm paths redirect to subdomain ([#156](https://github.com/ipfs/public-gateway-checker/issues/156)) ([b837a35](https://github.com/ipfs/public-gateway-checker/commit/b837a350576eb7e817a124a62984257c32ea68c3)) 475 | * remove heart ([#332](https://github.com/ipfs/public-gateway-checker/issues/332)) ([f61ec84](https://github.com/ipfs/public-gateway-checker/commit/f61ec842fd67447fdb85a4a14f1506abf858296a)) 476 | * update ipfs.ivoputzer.xyz gateway entry ([#152](https://github.com/ipfs/public-gateway-checker/issues/152)) ([4b760d9](https://github.com/ipfs/public-gateway-checker/commit/4b760d9497ae250f7cd65c41c4d22ece834e282c)) 477 | * update metrics collection banner to modal with management toggle settings ([#373](https://github.com/ipfs/public-gateway-checker/issues/373)) ([d925b36](https://github.com/ipfs/public-gateway-checker/commit/d925b36a3c9107840cc5b7d9f23622cd1a1f9c65)) 478 | * update redirect opt-out symbol to final version ([efd5dbf](https://github.com/ipfs/public-gateway-checker/commit/efd5dbfcf951e71d5b801bf444756e24b446c035)) 479 | 480 | 481 | ### Trivial Changes 482 | 483 | * **deps-dev:** bump aegir from 36.2.3 to 37.5.5 ([#305](https://github.com/ipfs/public-gateway-checker/issues/305)) ([1d62fc3](https://github.com/ipfs/public-gateway-checker/commit/1d62fc358f62548380c712df36bda2193cf91a77)) 484 | * **deps-dev:** bump aegir from 37.5.5 to 37.5.6 ([#316](https://github.com/ipfs/public-gateway-checker/issues/316)) ([d3cd9bd](https://github.com/ipfs/public-gateway-checker/commit/d3cd9bd003993fc955634e69015ab1820293ab68)) 485 | * **deps-dev:** bump browserslist from 4.19.3 to 4.21.4 ([#295](https://github.com/ipfs/public-gateway-checker/issues/295)) ([7850071](https://github.com/ipfs/public-gateway-checker/commit/785007163d7fa1d2c6f77433adcede5c1d2556a7)) 486 | * **deps-dev:** bump eslint-config-ipfs from 2.1.0 to 3.1.1 ([#300](https://github.com/ipfs/public-gateway-checker/issues/300)) ([f1bce91](https://github.com/ipfs/public-gateway-checker/commit/f1bce91c38315a65ba447aecef35173baef8b370)) 487 | * **deps-dev:** bump eslint-config-ipfs from 3.1.1 to 3.1.2 ([#315](https://github.com/ipfs/public-gateway-checker/issues/315)) ([0506c19](https://github.com/ipfs/public-gateway-checker/commit/0506c1935478c9fe1b95ee251d3a723437e01a27)) 488 | * **deps-dev:** bump ipfs from 0.62.1 to 0.64.2 ([#296](https://github.com/ipfs/public-gateway-checker/issues/296)) ([d47e503](https://github.com/ipfs/public-gateway-checker/commit/d47e503304794eefd95b76586835831376f7cc9a)) 489 | * **deps-dev:** bump ipfs from 0.64.2 to 0.65.0 ([#322](https://github.com/ipfs/public-gateway-checker/issues/322)) ([b400349](https://github.com/ipfs/public-gateway-checker/commit/b4003493f343ada61dbf0cba93d89750304c3bf7)) 490 | * **deps-dev:** bump typescript from 4.6.2 to 4.8.3 ([#293](https://github.com/ipfs/public-gateway-checker/issues/293)) ([c56afa1](https://github.com/ipfs/public-gateway-checker/commit/c56afa16264bb66fbde60baaa1cb31cfe8000737)) 491 | * **deps-dev:** bump typescript from 4.8.3 to 4.8.4 ([#304](https://github.com/ipfs/public-gateway-checker/issues/304)) ([899b4fc](https://github.com/ipfs/public-gateway-checker/commit/899b4fcf8256637567ce41fb711ae33000aba108)) 492 | * **deps:** bump @dutu/rate-limiter from v1.3.0 to v1.3.1 ([#299](https://github.com/ipfs/public-gateway-checker/issues/299)) ([5e598e8](https://github.com/ipfs/public-gateway-checker/commit/5e598e87d21ba7655de938b759e5db14ac004216)) 493 | * **deps:** bump aegir from 36.1.3 to 36.2.3 ([#202](https://github.com/ipfs/public-gateway-checker/issues/202)) ([8fa5851](https://github.com/ipfs/public-gateway-checker/commit/8fa5851bc90aa94bf061fe6b3689803fa8aa6a81)) 494 | * **deps:** bump jpeg-js from 0.4.3 to 0.4.4 ([#253](https://github.com/ipfs/public-gateway-checker/issues/253)) ([65f99f3](https://github.com/ipfs/public-gateway-checker/commit/65f99f3b593c5850e4e6b6809e0d05f2dc025081)) 495 | * **deps:** ipfs-http-client@58.0.1 ([#308](https://github.com/ipfs/public-gateway-checker/issues/308)) ([1bcda7c](https://github.com/ipfs/public-gateway-checker/commit/1bcda7cce199b4ea530aedaf1b1a052962ce6406)) 496 | * improve submission/PR info ([#119](https://github.com/ipfs/public-gateway-checker/issues/119)) ([a238f3f](https://github.com/ipfs/public-gateway-checker/commit/a238f3f39755900403bf6602775d031f0c5a1f33)) 497 | * improved security notes ([#151](https://github.com/ipfs/public-gateway-checker/issues/151)) ([5893f35](https://github.com/ipfs/public-gateway-checker/commit/5893f359f82a9f4f0c9c2b2361bd8df176449a66)), closes [#148](https://github.com/ipfs/public-gateway-checker/issues/148) [/github.com/ipfs/public-gateway-checker/pull/151#issuecomment-857193370](https://github.com/ipfs//github.com/ipfs/public-gateway-checker/pull/151/issues/issuecomment-857193370) 498 | * ipfs-geoip v5 ([0d8091e](https://github.com/ipfs/public-gateway-checker/commit/0d8091ef611cbe174c942d293100207f2121b902)) 499 | * ipfs-geoip@8.0.0 ([c4e8180](https://github.com/ipfs/public-gateway-checker/commit/c4e81805ab3f0221adf40afaa7840058bf31f3a7)) 500 | * readme cleanup ([798777e](https://github.com/ipfs/public-gateway-checker/commit/798777e4b6fccf5a29a0e49ce53b0495c22fdb44)) 501 | * remove dead hostnames ([#280](https://github.com/ipfs/public-gateway-checker/issues/280)) ([e861280](https://github.com/ipfs/public-gateway-checker/commit/e861280c368b44b05376dee103ea136bc2a67d0e)) 502 | * remove expired domains ([#179](https://github.com/ipfs/public-gateway-checker/issues/179)) ([16c9985](https://github.com/ipfs/public-gateway-checker/commit/16c99857e4f4cb67ff5a8bc617ec9fa44bdc8a6d)) 503 | * remove ipfs-zod.tv ([#234](https://github.com/ipfs/public-gateway-checker/issues/234)) ([6f79a80](https://github.com/ipfs/public-gateway-checker/commit/6f79a800e5b273e99214c0e0c37a3062049ea9c6)) 504 | * removed birds-are-nice.me ([#173](https://github.com/ipfs/public-gateway-checker/issues/173)) ([ff2e05c](https://github.com/ipfs/public-gateway-checker/commit/ff2e05c39098f7e3f0b2073496af0c91ce30e362)), closes [#172](https://github.com/ipfs/public-gateway-checker/issues/172) 505 | * removing my gateway for now ([#335](https://github.com/ipfs/public-gateway-checker/issues/335)) ([cf61e68](https://github.com/ipfs/public-gateway-checker/commit/cf61e685577a292e0f11319785127d03570906de)) 506 | * style formatting and linting fixes ([#366](https://github.com/ipfs/public-gateway-checker/issues/366)) ([a81d48b](https://github.com/ipfs/public-gateway-checker/commit/a81d48b75e684bb62216902309d40c92de31527d)) 507 | * Update .github/workflows/stale.yml [skip ci] ([5fc4a68](https://github.com/ipfs/public-gateway-checker/commit/5fc4a68733faa1054b9ae769fd913e77cba0ec3d)) 508 | * update readme with link to fleek ([#337](https://github.com/ipfs/public-gateway-checker/issues/337)) ([3dc5dbe](https://github.com/ipfs/public-gateway-checker/commit/3dc5dbe3563a2c767b03ce8e9764c233536c89ca)) 509 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is dual licensed under MIT and Apache-2.0. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/license-2.0 5 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPFS Public Gateway Checker 2 | 3 | **A site displaying public IPFS gateways and their online/offline status.** 4 | 5 | View the Public Gateway Checker in action 6 | 7 | * on GitHub Pages: https://ipfs.github.io/public-gateway-checker/ 8 | 9 | [![Screenshot of Public Gateway Checker](https://user-images.githubusercontent.com/157609/121263486-f7fb2800-c8b5-11eb-9061-0b6f586a6f25.png)](https://ipfs.github.io/public-gateway-checker/) 10 | 11 | ## SECURITY NOTES 12 | 13 | - The list contains gateways operated by various parties, coordinated by loose mutual consensus, without a central governing authority. Protocol Labs operates and is responsible for only two of the listed gateways: `ipfs.io` and `dweb.link`. 14 | - Gateways without origin isolation will be marked with ⚠️, indicating they are not safe for use cases that require private local storage of data or credentials. [Learn more](https://github.com/ipfs/public-gateway-checker/issues/150). 15 | 16 | ## Adding a new public gateway 17 | 18 | If you'd like to add a new public gateway, please edit `./gateways.json`: 19 | 20 | 1. Add the gateway's address to the **top** of the list 21 | 2. If you care about security of websites loaded via your gateway, make sure it is set up as a [subdomain gateway](https://docs.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway). See [config docs](https://github.com/ipfs/go-ipfs/blob/master/docs/config.md#gatewaypublicgateways) and [recipes](https://github.com/ipfs/go-ipfs/blob/master/docs/config.md#gateway-recipes) for go-ipfs, and [learn more here](https://github.com/ipfs/public-gateway-checker/issues/150). 22 | 23 | Then, submit a pull request for this change. Be sure to follow all the directions in the pull request template so your PR can be triaged as quickly as possible. 24 | 25 | ## Testing locally 26 | 27 | ```bash 28 | npm ci 29 | npm run build 30 | npm start 31 | ``` 32 | -------------------------------------------------------------------------------- /gateways.json: -------------------------------------------------------------------------------- 1 | [ 2 | "https://flk-ipfs.xyz", 3 | "https://ipfs.cyou", 4 | "https://dlunar.net", 5 | "https://storry.tv", 6 | "https://ipfs.io", 7 | "https://dweb.link", 8 | "https://gateway.pinata.cloud", 9 | "https://hardbin.com", 10 | "https://ipfs.runfission.com", 11 | "https://ipfs.eth.aragon.network", 12 | "https://nftstorage.link", 13 | "https://4everland.io", 14 | "https://w3s.link", 15 | "https://trustless-gateway.link" 16 | ] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ipfs/public-gateway-checker", 3 | "private": true, 4 | "version": "1.15.2", 5 | "description": "Checks which public gateways are online or not", 6 | "author": "Marcin Rataj ", 7 | "license": "Apache-2.0 OR MIT", 8 | "homepage": "https://github.com/ipfs/public-gateway-checker#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ipfs/public-gateway-checker.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/ipfs/public-gateway-checker/issues" 15 | }, 16 | "engines": { 17 | "node": ">=16.0.0", 18 | "npm": ">=7.0.0" 19 | }, 20 | "main": "src/index.ts", 21 | "files": [ 22 | "src", 23 | "dist" 24 | ], 25 | "eslintConfig": { 26 | "extends": "ipfs" 27 | }, 28 | "release": { 29 | "branches": [ 30 | "main" 31 | ], 32 | "plugins": [ 33 | [ 34 | "@semantic-release/commit-analyzer", 35 | { 36 | "preset": "conventionalcommits", 37 | "releaseRules": [ 38 | { 39 | "breaking": true, 40 | "release": "major" 41 | }, 42 | { 43 | "revert": true, 44 | "release": "patch" 45 | }, 46 | { 47 | "type": "feat", 48 | "release": "minor" 49 | }, 50 | { 51 | "type": "fix", 52 | "release": "patch" 53 | }, 54 | { 55 | "type": "chore", 56 | "release": "patch" 57 | }, 58 | { 59 | "type": "docs", 60 | "release": "patch" 61 | }, 62 | { 63 | "type": "test", 64 | "release": "patch" 65 | }, 66 | { 67 | "scope": "no-release", 68 | "release": false 69 | } 70 | ] 71 | } 72 | ], 73 | [ 74 | "@semantic-release/release-notes-generator", 75 | { 76 | "preset": "conventionalcommits", 77 | "presetConfig": { 78 | "types": [ 79 | { 80 | "type": "feat", 81 | "section": "Features" 82 | }, 83 | { 84 | "type": "fix", 85 | "section": "Bug Fixes" 86 | }, 87 | { 88 | "type": "chore", 89 | "section": "Trivial Changes" 90 | }, 91 | { 92 | "type": "docs", 93 | "section": "Trivial Changes" 94 | }, 95 | { 96 | "type": "test", 97 | "section": "Tests" 98 | } 99 | ] 100 | } 101 | } 102 | ], 103 | "@semantic-release/changelog", 104 | "@semantic-release/npm", 105 | "@semantic-release/github", 106 | "@semantic-release/git" 107 | ] 108 | }, 109 | "scripts": { 110 | "start": "npm run build && npx -y serve -l 3000 dist", 111 | "copy-assets": "cp-cli src/index.html dist/index.html && cp-cli src/styles.css dist/styles.css", 112 | "lint": "aegir lint && npx -y standard && node scripts/beautify.mjs", 113 | "lint:fix": "aegir lint --fix && npx -y standard --fix && node scripts/beautify.mjs --fix", 114 | "release": "aegir release", 115 | "sw": "node scripts/build-sw.mjs", 116 | "test-gateways": "node scripts/test-gateways.mjs", 117 | "build": "aegir build && npm run copy-assets && npm run sw", 118 | "test-TBD": "aegir test", 119 | "test-TBD:node": "aegir test --target node", 120 | "test-TBD:browser": "aegir test --target browser" 121 | }, 122 | "dependencies": { 123 | "@dutu/rate-limiter": "github:dutu/rate-limiter#v1.3.1", 124 | "url-ponyfill": "^0.5.10" 125 | }, 126 | "devDependencies": { 127 | "aegir": "^42.2.3", 128 | "browserslist": "^4.23.0", 129 | "check-aegir-project": "^1.1.1", 130 | "cp-cli": "^2.0.0", 131 | "eslint-config-ipfs": "^6.0.1", 132 | "eslint-plugin-compat": "^6.0.0", 133 | "fetch-ponyfill": "^7.1.0", 134 | "ipfs-geoip": "^9.2.0", 135 | "js-beautify": "^1.15.1", 136 | "ts-node": "^10.9.2", 137 | "typescript": "^5.4.4", 138 | "workbox-build": "7.0.0", 139 | "workbox-window": "7.0.0", 140 | "zod": "^3.22.2" 141 | }, 142 | "aegir": { 143 | "tsRepo": true, 144 | "release": { 145 | "build": false 146 | }, 147 | "docs": { 148 | "publish": true, 149 | "entryPoint": "src/index.ts" 150 | } 151 | }, 152 | "browserslist": [ 153 | "defaults", 154 | "not op_mini all", 155 | "not IE 11" 156 | ], 157 | "todo": [ 158 | "add browserlist support", 159 | "Add eslint" 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /scripts/beautify.mjs: -------------------------------------------------------------------------------- 1 | import jsBeautify from 'js-beautify' 2 | import { promises as fs } from 'fs' 3 | 4 | const saveFile = process.argv.slice(2).includes('--fix') 5 | 6 | const files = { 7 | 'src/index.html': jsBeautify.html, 8 | 'src/styles.css': jsBeautify.css 9 | } 10 | 11 | async function beautificationFn (filePath, fn) { 12 | console.log(`Checking ${filePath} formatting...`) 13 | const sourceCode = await fs.readFile(filePath, 'utf8') 14 | 15 | console.log(`Beautifying ${filePath}...`) 16 | const beautifiedCode = fn(sourceCode, { 17 | indent_size: 2, 18 | space_in_empty_paren: true, 19 | indent_char: ' ', 20 | indent_with_tabs: false, 21 | editorconfig: false, 22 | eol: '\n', 23 | end_with_newline: true, 24 | indent_level: 0 25 | }) 26 | 27 | // lint mode.. fail if not beautified 28 | if (sourceCode === beautifiedCode) { 29 | console.log(`${filePath} is already beautified`) 30 | return 31 | } else if (!saveFile) { 32 | throw new Error(`${filePath} is not beautified`) 33 | } 34 | 35 | if (saveFile) { 36 | console.log(`Saving beautified ${filePath}`) 37 | await fs.writeFile(filePath, beautifiedCode, 'utf8') 38 | } 39 | } 40 | 41 | for (const [filePath, fn] of Object.entries(files)) { 42 | await beautificationFn(filePath, fn) 43 | } 44 | -------------------------------------------------------------------------------- /scripts/build-sw.mjs: -------------------------------------------------------------------------------- 1 | import { generateSW } from 'workbox-build' 2 | 3 | generateSW({ 4 | globDirectory: 'dist/', 5 | globPatterns: [ 6 | '**/*.{css,woff2,png,svg,jpg,js}' 7 | ], 8 | globIgnores: [ 9 | 'src/**/*' 10 | ], 11 | runtimeCaching: [ 12 | { 13 | urlPattern: /\.(?:png|jpg|jpeg|svg)$/, 14 | handler: 'NetworkFirst' 15 | } 16 | ], 17 | swDest: 'dist/sw.js' 18 | }) 19 | -------------------------------------------------------------------------------- /scripts/test-gateways.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import dns from 'node:dns/promises' 3 | import http from 'node:http' 4 | import https from 'node:https' 5 | 6 | const timeout = 5000 7 | 8 | function makeRequest (url) { 9 | return new Promise((resolve, reject) => { 10 | let responseBody = '' 11 | const opts = { 12 | method: 'GET', 13 | headers: { 14 | Accept: 'application/vnd.ipld.raw' 15 | } 16 | } 17 | const protocolModule = new URL(url).protocol === 'https:' ? https : http 18 | const req = protocolModule.request(url, opts, (res) => { 19 | if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { 20 | return makeRequest(new URL(res.headers.location), opts).then(resolve, reject) 21 | } 22 | res.on('data', (chunk) => { 23 | responseBody += chunk 24 | }) 25 | res.on('end', () => { 26 | resolve({ statusCode: res.statusCode, body: responseBody, headers: res.headers }) 27 | }) 28 | }) 29 | 30 | req.on('error', (error) => { 31 | reject(error) 32 | }) 33 | 34 | req.setTimeout(timeout, () => { 35 | req.destroy() 36 | reject(new Error(`HTTP GET timed out after ${timeout}ms`)) 37 | }) 38 | 39 | req.end() 40 | }) 41 | } 42 | 43 | function trim (str) { 44 | return str.length > 7 ? str.slice(0, 7) + '...' : str 45 | } 46 | 47 | (async () => { 48 | const gateways = JSON.parse(await fs.readFile('./gateways.json')) 49 | const resolvableGateways = [] 50 | 51 | for (const gw of gateways) { 52 | const gwUrl = new URL(gw) 53 | if (gwUrl.hostname.endsWith('.onion')) { 54 | resolvableGateways.push(gw) 55 | continue 56 | } 57 | try { 58 | await dns.lookup(gwUrl.hostname) 59 | 60 | const url = `${gw}/ipfs/bafkqablimvwgy3y?format=raw` 61 | 62 | const { statusCode, body, headers } = await makeRequest(url) 63 | if (statusCode < 200 || statusCode >= 400) { 64 | console.log(`\x1b[31mERROR\x1b[0m: ${gwUrl.hostname} received ${statusCode} response for ${url}. Expected status 2XX or 3XX`) 65 | continue 66 | } 67 | if (body !== 'hello') { 68 | console.log(`\x1b[31mERROR\x1b[0m: ${gwUrl.hostname} returned body '${trim(body)}' does not match test plain text block for ${url}. Expected 'hello'`) 69 | continue 70 | } 71 | const ctype = headers['content-type'] 72 | if (ctype !== 'application/vnd.ipld.raw') { 73 | console.log(`\x1b[33mWARN\x1b[0m: ${gwUrl.hostname} returned Content-Type: ${ctype} for ${url}. Expected application/vnd.ipld.raw`) 74 | // body matches, so gateway works, but does not support trustless responses, which should be standard, thus warning 75 | } else { 76 | console.log(`\x1b[32mOK\x1b[0m: ${gwUrl.hostname}`) 77 | } 78 | resolvableGateways.push(gw) 79 | } catch (e) { 80 | console.error(`\x1b[31mERROR\x1b[0m: ${gwUrl.hostname} DNS or HTTP failure:`, e.message) 81 | } 82 | } 83 | 84 | await fs.writeFile('./gateways.json', JSON.stringify(resolvableGateways, null, ' ')) 85 | })() 86 | -------------------------------------------------------------------------------- /src/CheckBase.ts: -------------------------------------------------------------------------------- 1 | import { UiComponent } from './UiComponent' 2 | import type { Checker } from './Checker' 3 | import type { GatewayNode } from './GatewayNode' 4 | import type { Tag } from './Tag' 5 | import type { Checkable } from './types' 6 | 7 | /** 8 | * Base Check functionality 9 | */ 10 | class CheckBase extends UiComponent implements Checkable { 11 | _className = 'Not-set' 12 | _tagName = 'Not-set' 13 | 14 | get className (): string { 15 | return this._className 16 | } 17 | 18 | get tagName (): string { 19 | return this._tagName 20 | } 21 | 22 | constructor (protected readonly parent: Checker | GatewayNode, ...tagParams: ConstructorParameters) { 23 | super(parent, ...tagParams) 24 | } 25 | 26 | check (): void { 27 | 28 | } 29 | 30 | checked (): void { 31 | 32 | } 33 | 34 | onerror (): void { 35 | this.tag.err() 36 | } 37 | } 38 | 39 | export { CheckBase } 40 | -------------------------------------------------------------------------------- /src/Checker.ts: -------------------------------------------------------------------------------- 1 | import { GatewayNode } from './GatewayNode' 2 | import { Log } from './Log' 3 | import { Results } from './Results' 4 | import { Stats } from './Stats' 5 | 6 | const log = new Log('Checker') 7 | 8 | class Checker { 9 | public readonly element: HTMLElement 10 | public readonly nodes: GatewayNode[] = [] 11 | private readonly stats: Stats 12 | private readonly results: Results 13 | constructor () { 14 | const element = document.getElementById('checker') 15 | if (element == null) { 16 | throw new Error('Element with Id "checker" not found.') 17 | } 18 | this.element = element 19 | this.stats = new Stats(this) 20 | this.results = new Results(this) 21 | this.updateStats = this.updateStats.bind(this) 22 | } 23 | 24 | private updateStats (): void { 25 | this.stats.update() 26 | } 27 | 28 | async checkGateways (gateways: string[]): Promise { 29 | const allChecks: Array> = [] 30 | for (const gateway of gateways) { 31 | const node = new GatewayNode(this.results, gateway, this.nodes.length) 32 | this.nodes.push(node) 33 | this.results.append(node.tag) 34 | // void node.check() 35 | setTimeout(() => { 36 | allChecks.push(node.check().catch((err) => { log.error(err) }).finally(this.updateStats)) 37 | }, 100 * this.nodes.length) 38 | } 39 | // await Promise.all(allChecks).finally(this.updateStats) 40 | } 41 | } 42 | 43 | export { Checker } 44 | -------------------------------------------------------------------------------- /src/Cors.ts: -------------------------------------------------------------------------------- 1 | import fetchPonyfill from 'fetch-ponyfill' 2 | import { CheckBase } from './CheckBase' 3 | import { Log } from './Log' 4 | import { HASH_STRING, HASH_TO_TEST } from './constants' 5 | import type { GatewayNode } from './GatewayNode' 6 | import type { Checkable } from './types' 7 | 8 | const { fetch } = fetchPonyfill() 9 | 10 | const log = new Log('Cors') 11 | 12 | class Cors extends CheckBase implements Checkable { 13 | _className = 'Cors' 14 | _tagName = 'div' 15 | constructor (protected parent: GatewayNode) { 16 | super(parent, 'div', 'Cors') 17 | } 18 | 19 | async check (): Promise { 20 | const now = Date.now() 21 | const gatewayAndHash = `${this.parent.gateway}/ipfs/${HASH_TO_TEST}` 22 | const testUrl = `${gatewayAndHash}?now=${now}#x-ipfs-companion-no-redirect` 23 | // response body can be accessed only if fetch was executed when 24 | // liberal CORS is present (eg. '*') 25 | try { 26 | const response = await fetch(testUrl) 27 | const { status } = response 28 | const text = await response.text() 29 | this.tag.title = `Response code: ${status}` 30 | if (HASH_STRING === text.trim()) { 31 | // this.parent.checked() 32 | this.tag.asterisk() 33 | this.parent.tag.classList.add('cors') 34 | } else { 35 | log.debug('The response text did not match the expected string') 36 | this.onerror() 37 | throw new Error(`URL '${testUrl} does not support CORS`) 38 | } 39 | } catch (err) { 40 | log.error(err) 41 | this.onerror() 42 | throw err 43 | } 44 | } 45 | 46 | checked (): void { 47 | log.warn('Not implemented yet') 48 | } 49 | 50 | onerror (): void { 51 | this.tag.empty() 52 | } 53 | } 54 | 55 | export { Cors } 56 | -------------------------------------------------------------------------------- /src/Flag.ts: -------------------------------------------------------------------------------- 1 | import { TokenBucketLimiter } from '@dutu/rate-limiter' 2 | import { lookup as IpfsGeoIpLookup } from 'ipfs-geoip' 3 | import { Log } from './Log' 4 | import { UiComponent } from './UiComponent' 5 | import { DEFAULT_IPFS_GATEWAY } from './constants' 6 | import type { GatewayNode } from './GatewayNode' 7 | import type { DnsQueryResponse } from './types' 8 | 9 | const log = new Log('Flag') 10 | 11 | class Flag extends UiComponent { 12 | /** 13 | */ 14 | public static readonly googleLimiter = new TokenBucketLimiter({ bucketSize: 1, tokensPerInterval: 1, interval: 1000 * 2, stopped: true }) 15 | public static readonly cloudFlareLimiter = new TokenBucketLimiter({ bucketSize: 1, tokensPerInterval: 1, interval: 1000 * 2, stopped: true }) 16 | 17 | constructor (protected parent: GatewayNode, private readonly hostname: string) { 18 | super(parent, 'div', 'Flag') 19 | } 20 | 21 | async check (): Promise { 22 | let ask = true 23 | 24 | try { 25 | const savedSTR = localStorage.getItem(this.hostname) 26 | if (savedSTR != null) { 27 | const saved = JSON.parse(savedSTR) 28 | const now = Date.now() 29 | const savedTime = saved.time 30 | const elapsed = now - savedTime 31 | const expiration = 7 * 24 * 60 * 60 * 1000 // 7 days 32 | if (elapsed < expiration) { 33 | ask = false 34 | this.onResponse(saved) 35 | } 36 | } 37 | } catch (e) { 38 | log.error(`error while getting savedSTR for ${this.hostname}`, e) 39 | this.onError() 40 | throw e 41 | } 42 | 43 | if (ask) { 44 | this.startLimiters() 45 | const url = await this.waitForAvailableEndpoint() 46 | await this.dnsRequest(url) 47 | } 48 | } 49 | 50 | private startLimiters (): void { 51 | if (Flag.googleLimiter.isStopped === true) { 52 | Flag.googleLimiter.start() 53 | } 54 | if (Flag.cloudFlareLimiter.isStopped === true) { 55 | Flag.cloudFlareLimiter.start() 56 | } 57 | } 58 | 59 | async waitForAvailableEndpoint (): Promise { 60 | const url: string | null = await Promise.race([ 61 | Flag.googleLimiter.awaitTokens(1).then(() => Flag.googleLimiter.tryRemoveTokens(1)).then((tokenAvailable: boolean) => { 62 | if (tokenAvailable) { 63 | return `https://dns.google/resolve?name=${this.hostname}&type=A` 64 | } 65 | }), 66 | Flag.cloudFlareLimiter.awaitTokens(1).then(() => Flag.cloudFlareLimiter.tryRemoveTokens(1)).then((tokenAvailable: boolean) => { 67 | if (tokenAvailable) { 68 | return `https://cloudflare-dns.com/dns-query?name=${this.hostname}&type=A` 69 | } 70 | }), 71 | ]) 72 | if (url == null) { 73 | // No available tokens... 74 | log.info('we awaited tokens, but could not retrieve any.. restarting dnsRequest') 75 | return this.waitForAvailableEndpoint() 76 | } else { 77 | return url 78 | } 79 | } 80 | 81 | private async dnsRequest (url: string): Promise { 82 | try { 83 | const response = await fetch(url, { 84 | method: 'GET', 85 | headers: { 86 | Accept: 'application/dns-json', 87 | }, 88 | }) 89 | const responseJson = await response.json() 90 | 91 | await this.handleDnsQueryResponse(responseJson) 92 | } catch (err) { 93 | log.error('problem submitting DNS request', url, err) 94 | this.onError() 95 | } 96 | } 97 | 98 | async handleDnsQueryResponse (response: DnsQueryResponse): Promise { 99 | if (response.Answer == null) { 100 | log.error('Response does not contain the "Answer" property:', response) 101 | this.onError() 102 | return 103 | } 104 | let ip = null 105 | for (let i = 0; i < response.Answer.length && ip == null; i++) { 106 | const answer = response.Answer[i] 107 | if (answer.type === 1) { 108 | ip = answer.data 109 | } 110 | } 111 | if (ip != null) { 112 | try { 113 | const geoipResponse = await IpfsGeoIpLookup(DEFAULT_IPFS_GATEWAY, ip) 114 | 115 | if (geoipResponse?.country_code != null) { 116 | this.onResponse(geoipResponse) 117 | geoipResponse.time = Date.now() 118 | const responseSTR = JSON.stringify(geoipResponse) 119 | localStorage.setItem(this.hostname, responseSTR) 120 | } else { 121 | log.error('geoipResponse.country_code is null') 122 | } 123 | } catch (e) { 124 | log.error(`error while getting DNS A record for ${this.hostname}`, e) 125 | this.onError() 126 | } 127 | } else { 128 | log.error('IP is still null', response) 129 | } 130 | } 131 | 132 | private onError (): void { 133 | this.tag.empty() 134 | } 135 | 136 | onResponse (response: IpfsGeoip.LookupResponse): void { 137 | this.tag.style.setProperty('background-image', `url('https://ipfs.io/ipfs/QmaYjj5BHGAWfopTdE8ESzypbuthsZqTeqz9rEuh3EJZi6/${response.country_code.toLowerCase()}.svg')`) 138 | this.tag.title = response.country_name 139 | this.tag.empty() // remove textContent icon since we're using a background image 140 | } 141 | } 142 | 143 | export { Flag } 144 | -------------------------------------------------------------------------------- /src/GatewayNode.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url-ponyfill' 2 | import { Cors } from './Cors' 3 | import { Flag } from './Flag' 4 | import { IPNSCheck } from './Ipns' 5 | import { Log } from './Log' 6 | import { Origin } from './Origin' 7 | import { Status } from './Status' 8 | import { Trustless } from './Trustless' 9 | import { UiComponent } from './UiComponent' 10 | import { HASH_TO_TEST } from './constants' 11 | import { gatewayHostname } from './gatewayHostname' 12 | import type { Results } from './Results' 13 | 14 | const log = new Log('GatewayNode') 15 | 16 | class GatewayNode extends UiComponent /* implements Checkable */ { 17 | // tag: Tag 18 | status: Status 19 | cors: Cors 20 | ipns: IPNSCheck 21 | origin: Origin 22 | trustless: Trustless 23 | link: HTMLDivElement & { url?: URL } 24 | flag: Flag 25 | took: HTMLDivElement 26 | gateway: string 27 | index: unknown 28 | checkingTime: number 29 | 30 | atLeastOneSuccess = false 31 | 32 | constructor (readonly parent: Results, gateway: string, index: unknown) { 33 | super(parent, 'div', 'Node') 34 | 35 | this.tag.empty() 36 | 37 | this.tag.style.order = Date.now().toString() 38 | 39 | this.status = new Status(this) 40 | this.tag.append(this.status.tag) 41 | 42 | this.cors = new Cors(this) 43 | this.tag.append(this.cors.tag) 44 | 45 | this.ipns = new IPNSCheck(this) 46 | this.tag.append(this.ipns.tag) 47 | 48 | this.origin = new Origin(this) 49 | this.tag.append(this.origin.tag) 50 | 51 | this.trustless = new Trustless(this) 52 | this.tag.append(this.trustless.tag) 53 | 54 | this.link = document.createElement('div') 55 | const gatewayAndHash = `${gateway}/ipfs/${HASH_TO_TEST}` 56 | this.link.url = new URL(gatewayAndHash) 57 | this.link.textContent = gatewayHostname(this.link.url) 58 | this.link.className = 'Link' 59 | 60 | this.flag = new Flag(this, this.link.textContent) 61 | // this.link.prepend(this.flag.tag.element) 62 | this.tag.append(this.flag.tag) 63 | this.tag.append(this.link) 64 | 65 | this.took = document.createElement('div') 66 | this.took.className = 'Took' 67 | this.tag.append(this.took) 68 | 69 | this.gateway = gateway 70 | this.index = index 71 | this.checkingTime = 0 72 | } 73 | 74 | public async check (): Promise { 75 | this.checkingTime = Date.now() 76 | // const onFailedCheck = () => { this.status.down = true } 77 | // const onSuccessfulCheck = () => { this.status.up = true } 78 | void this.flag.check().then(() => { log.debug(this.gateway, 'Flag success') }) 79 | const onlineChecks = [ 80 | // this.flag.check().then(() => log.debug(this.gateway, 'Flag success')), 81 | this.status.check().then(() => { log.debug(this.gateway, 'Status success') }).then(this.onSuccessfulCheck.bind(this)), 82 | this.cors.check().then(() => { log.debug(this.gateway, 'CORS success') }).then(this.onSuccessfulCheck.bind(this)), 83 | this.ipns.check().then(() => { log.debug(this.gateway, 'IPNS success') }).then(this.onSuccessfulCheck.bind(this)), 84 | this.origin.check().then(() => { log.debug(this.gateway, 'Origin success') }).then(this.onSuccessfulCheck.bind(this)), 85 | this.trustless.check().then( 86 | () => { log.debug(this.gateway, 'Trustless success') }).then(this.onSuccessfulCheck.bind(this)), 87 | ] 88 | 89 | // we care only about the fastest method to return a success 90 | // Promise.race(onlineChecks).catch((err) => { 91 | // log.error('Promise race error', err) 92 | // }) 93 | 94 | // await Promise.all(onlineChecks).catch(onFailedCheck) 95 | await Promise.allSettled(onlineChecks).then((results) => results.map((result) => { 96 | return result.status 97 | })).then((statusArray) => { 98 | if (statusArray.includes('fulfilled')) { 99 | // At least promise was successful, which means the gateway is online 100 | log.debug(`For gateway '${this.gateway}', at least one promise was successfully fulfilled`) 101 | log.debug(this.gateway, 'this.status.up: ', this.status.up) 102 | log.debug(this.gateway, 'this.status.down: ', this.status.down) 103 | this.status.up = true 104 | log.debug(this.gateway, 'this.status.up: ', this.status.up) 105 | log.debug(this.gateway, 'this.status.down: ', this.status.down) 106 | } else { 107 | // No promise was successful, the gateway is down. 108 | this.status.down = true 109 | log.debug(`For gateway '${this.gateway}', all promises were rejected`) 110 | } 111 | }) 112 | } 113 | 114 | private onSuccessfulCheck (): void { 115 | if (!this.atLeastOneSuccess) { 116 | log.info(`For gateway '${this.gateway}', at least one check was successful`) 117 | this.atLeastOneSuccess = true 118 | this.status.up = true 119 | const url = this.link.url 120 | if (url != null) { 121 | const host = gatewayHostname(url) 122 | // const anchor = document.createElement('a') 123 | // anchor.title = host 124 | // anchor.href = `${url.toString()}#x-ipfs-companion-no-redirect` 125 | // anchor.target = '_blank' 126 | // anchor.textContent = host 127 | // this.flag.tag.element.remove() 128 | // this.link.textContent = '' 129 | // this.link.append(this.flag.tag.element, anchor) 130 | this.link.innerHTML = `${host}` 131 | } 132 | const ms = Date.now() - this.checkingTime 133 | this.tag.style.order = ms.toString() 134 | const s = (ms / 1000).toFixed(2) 135 | this.took.textContent = `${s}s` 136 | } 137 | } 138 | 139 | // private checked () { 140 | // if (!this.doneChecking) { 141 | // this.doneChecking = true 142 | // // this.status.checked() 143 | // // this.parent.checked() 144 | // } else { 145 | // log.warn('"checked" method called more than once.. potential logic error') 146 | // } 147 | // } 148 | 149 | onerror (): void { 150 | this.tag.err() 151 | } 152 | } 153 | 154 | export { GatewayNode } 155 | -------------------------------------------------------------------------------- /src/Ipns.ts: -------------------------------------------------------------------------------- 1 | import fetchPonyfill from 'fetch-ponyfill' 2 | import { CheckBase } from './CheckBase' 3 | import { Log } from './Log' 4 | import { IPNS_PATH_TO_TEST } from './constants' 5 | import type { GatewayNode } from './GatewayNode' 6 | import type { Checkable } from './types' 7 | 8 | const { fetch } = fetchPonyfill() 9 | 10 | const log = new Log('Ipns') 11 | 12 | class IPNSCheck extends CheckBase implements Checkable { 13 | _className = 'Ipns' 14 | _tagName = 'div' 15 | constructor (protected parent: GatewayNode) { 16 | super(parent, 'div', 'Ipns') 17 | } 18 | 19 | async check (): Promise { 20 | const now = Date.now() 21 | // Since gateway URLs are hard coded with /ipfs/, we need to parse URLs and override the path to /ipns/. 22 | const gatewayUrl = new URL(this.parent.gateway) 23 | gatewayUrl.pathname = IPNS_PATH_TO_TEST 24 | const testUrl = `${gatewayUrl.href}?now=${now}#x-ipfs-companion-no-redirect` 25 | try { 26 | const response = await fetch(testUrl) 27 | if (response.status === 200) { 28 | this.tag.win() 29 | } else { 30 | log.debug(`${this.parent.gateway} does not support IPNS`) 31 | throw new Error(`URL '${testUrl} is not reachable`) 32 | } 33 | } catch (err) { 34 | log.error(err) 35 | this.onerror() 36 | throw err 37 | } 38 | } 39 | 40 | checked (): void { 41 | log.warn('Not implemented yet') 42 | } 43 | 44 | onerror (): void { 45 | this.tag.err() 46 | } 47 | } 48 | 49 | export { IPNSCheck } 50 | -------------------------------------------------------------------------------- /src/Log.ts: -------------------------------------------------------------------------------- 1 | type Console = typeof console 2 | 3 | /** 4 | * This class' sole purpose is to avoid cluttering the codebase with `eslint-disable-line no-console` comments 5 | * 6 | * When using this class to log errors or messages, one can assume it's intentional and principled. 7 | */ 8 | class Log { 9 | constructor (private readonly namespace?: string) {} 10 | 11 | /** 12 | * The log method's generic typing allows it to only accept 13 | * 14 | * @param method - log 15 | * @param args 16 | */ 17 | private log>>(method: M, ...args: Parameters): void { 18 | const [msg, ...optionalParams] = args 19 | const prefix = this.namespace != null ? `${this.namespace}.${method}: ` : '' 20 | 21 | // eslint-disable-next-line no-console 22 | console[method](`${prefix}${msg as string}`, ...optionalParams) 23 | } 24 | 25 | debug (...args: Parameters): void { 26 | this.log('debug', ...args) 27 | } 28 | 29 | info (...args: Parameters): void { 30 | this.log('info', ...args) 31 | } 32 | 33 | warn (...args: Parameters): void { 34 | this.log('warn', ...args) 35 | } 36 | 37 | error (...args: Parameters): void { 38 | this.log('error', ...args) 39 | } 40 | } 41 | 42 | export { Log } 43 | -------------------------------------------------------------------------------- /src/Origin.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url-ponyfill' 2 | import { Log } from './Log' 3 | import { Tag } from './Tag' 4 | import { checkViaImgSrc } from './checkViaImgSrc' 5 | import { IMG_HASH } from './constants' 6 | import { expectSubdomainRedirect } from './expectSubdomainRedirect' 7 | import type { GatewayNode } from './GatewayNode' 8 | 9 | const log = new Log('Origin') 10 | 11 | class Origin { 12 | tag: Tag 13 | constructor (public parent: GatewayNode) { 14 | this.tag = new Tag('div', 'Origin') 15 | } 16 | 17 | async check (): Promise { 18 | // we are unable to check url after subdomain redirect because some gateways 19 | // may not have proper CORS in place. instead, we manually construct subdomain 20 | // URL and check if it loading known image works 21 | const gwUrl = new URL(this.parent.gateway) 22 | // const imgPathUrl = new URL(`${gwUrl.protocol}//${gwUrl.hostname}/ipfs/${IMG_HASH}?now=${now}&filename=1x1.png#x-ipfs-companion-no-redirect`) 23 | const imgSubdomainUrl = new URL(`${gwUrl.protocol}//${IMG_HASH}.ipfs.${gwUrl.hostname}/?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`) 24 | const imgRedirectedPathUrl = new URL(`${gwUrl.protocol}//${gwUrl.hostname}/ipfs/${IMG_HASH}?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`) 25 | await checkViaImgSrc(imgSubdomainUrl) 26 | .then(async () => expectSubdomainRedirect(imgRedirectedPathUrl)) 27 | .then(() => { 28 | this.tag.win(imgSubdomainUrl.toString()) 29 | this.parent.tag.classList.add('origin') 30 | // this.parent.checked() 31 | }) 32 | .catch((err) => { this.onerror(err) }) 33 | } 34 | 35 | onerror (err: Error): void { 36 | log.error(err) 37 | this.tag.err() 38 | throw err 39 | } 40 | } 41 | 42 | export { Origin } 43 | -------------------------------------------------------------------------------- /src/Results.ts: -------------------------------------------------------------------------------- 1 | import type { Checker } from './Checker' 2 | import type { Tag } from './Tag' 3 | 4 | class Results { 5 | append (tag: Tag): void { 6 | this.element.append(tag.element) 7 | } 8 | 9 | public readonly element: HTMLElement 10 | constructor (readonly parent: Checker) { 11 | const element = document.getElementById('checker.results') 12 | if (element == null) { 13 | throw new Error('Element with Id "checker.results" not found.') 14 | } 15 | this.element = element 16 | } 17 | } 18 | 19 | export { Results } 20 | -------------------------------------------------------------------------------- /src/Stats.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from './Tag' 2 | import { UiComponent } from './UiComponent' 3 | import type { Checker } from './Checker' 4 | 5 | class Stats extends UiComponent { 6 | gateways: HTMLDivElement 7 | totals: HTMLDivElement 8 | constructor (readonly parent: Checker) { 9 | super(parent) 10 | const statsElement = document.getElementById('checker.stats') 11 | if (statsElement == null) { 12 | throw new Error('Could not find element with Id "checker.stats"') 13 | } 14 | this.tag = Tag.fromElement(statsElement) 15 | 16 | this.gateways = document.createElement('div') 17 | this.gateways.textContent = '0/0 tested' 18 | this.gateways.className = 'Gateways' 19 | this.tag.append(this.gateways) 20 | 21 | this.totals = document.createElement('div') 22 | this.totals.textContent = '0 online' 23 | this.totals.className = 'Totals' 24 | this.tag.append(this.totals) 25 | } 26 | 27 | public update (): void { 28 | let up = 0 29 | let down = 0 30 | for (const savedNode of this.parent.nodes) { 31 | if (savedNode.status.up) { 32 | up += 1 33 | } else if (savedNode.status.down) { 34 | down += 1 35 | } 36 | } 37 | this.gateways.textContent = `${up + down}/${this.parent.nodes.length} tested` 38 | this.totals.textContent = `${up} online` 39 | } 40 | } 41 | 42 | export { Stats } 43 | -------------------------------------------------------------------------------- /src/Status.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url-ponyfill' 2 | import { Log } from './Log' 3 | import { UiComponent } from './UiComponent' 4 | import { checkViaImgSrc } from './checkViaImgSrc' 5 | import { IMG_HASH } from './constants' 6 | import type { GatewayNode } from './GatewayNode' 7 | 8 | const log = new Log('Status') 9 | 10 | class Status extends UiComponent { 11 | private _up: boolean = false 12 | private _down: boolean = false 13 | constructor (readonly parent: GatewayNode) { 14 | super(parent, 'div', 'Status') 15 | } 16 | 17 | async check (): Promise { 18 | // test by loading subresource via img.src (path will work on both old and subdomain gws) 19 | const gwUrl = new URL(this.parent.gateway) 20 | const imgPathUrl = new URL(`${gwUrl.protocol}//${gwUrl.hostname}/ipfs/${IMG_HASH}?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`) 21 | await checkViaImgSrc(imgPathUrl).catch((err) => { 22 | if (err != null) { 23 | log.error(this.parent.gateway, err) 24 | } 25 | // this.down = true 26 | // we check this because the gateway could be already checked by CORS before onerror executes 27 | // and, even though it is failing here, we know it is UP 28 | // if (!this.up) { 29 | // this.down = false 30 | // // this.tag.textContent = '❌' 31 | // this.tag.lose() 32 | // // this.parent.failed() 33 | // } 34 | throw err 35 | }) 36 | } 37 | 38 | get down (): boolean { 39 | return this._down 40 | } 41 | 42 | set down (value: boolean) { 43 | if (!this.up && !this.down) { 44 | this._down = true 45 | this.tag.lose() 46 | } 47 | } 48 | 49 | get up (): boolean { 50 | return this._up 51 | } 52 | 53 | set up (value: boolean) { 54 | if (!this.up && !this.down) { 55 | this._up = true 56 | this.tag.global() 57 | this.parent.tag.classList.add('online') 58 | } 59 | } 60 | 61 | // checked () { 62 | // // this.up = true 63 | // // this.tag.global() 64 | // } 65 | 66 | onerror (): void { 67 | throw new Error('Not implemented') 68 | } 69 | } 70 | 71 | export { Status } 72 | -------------------------------------------------------------------------------- /src/Tag.ts: -------------------------------------------------------------------------------- 1 | import { TagStatus } from './TagStatus' 2 | 3 | type TagClasses = 'Cors' | 'Flag' | 'Ipns' | 'Node' | 'Origin' | 'Status' | 'Trustless' 4 | 5 | type TagContent = TagStatus 6 | 7 | class Tag { 8 | element: HTMLElement 9 | constructor (tagName: keyof HTMLElementTagNameMap = 'div', className: TagClasses | undefined = undefined, textContent: TagContent = TagStatus.pending) { 10 | const element = document.createElement(tagName) 11 | this.element = element 12 | 13 | if (className != null) { 14 | this.className = className 15 | } 16 | this.textContent = textContent 17 | } 18 | 19 | public static fromElement (element: HTMLElement): Tag { 20 | const tag = new Tag('div') 21 | tag.element = element 22 | 23 | return tag 24 | } 25 | 26 | /** 27 | * Use the below functions to keep displays consistent 28 | */ 29 | asterisk (): void { 30 | this.textContent = TagStatus.asterisk 31 | } 32 | 33 | lose (): void { 34 | this.textContent = TagStatus.failed 35 | } 36 | 37 | win (url?: string): void { 38 | if (url != null) { 39 | this.textContent = TagStatus.empty 40 | const linkToImageSubdomain = document.createElement('a') 41 | linkToImageSubdomain.href = url 42 | linkToImageSubdomain.target = '_blank' 43 | linkToImageSubdomain.textContent = TagStatus.successful 44 | this.element.title = url 45 | this.element.appendChild(linkToImageSubdomain) 46 | } else { 47 | this.textContent = TagStatus.successful 48 | } 49 | } 50 | 51 | global (): void { 52 | this.textContent = TagStatus.global 53 | } 54 | 55 | err (): void { 56 | this.textContent = TagStatus.caution 57 | } 58 | 59 | empty (): void { 60 | this.textContent = TagStatus.empty 61 | } 62 | 63 | get style (): CSSStyleDeclaration { 64 | return this.element.style 65 | } 66 | 67 | append (child: string | Node | Tag): void { 68 | if (child instanceof Tag) { 69 | child = child.element 70 | } 71 | this.element.append(child) 72 | } 73 | 74 | get classList (): DOMTokenList { 75 | return this.element.classList 76 | } 77 | 78 | // eslint-disable-next-line accessor-pairs 79 | set title (newTitle: string) { 80 | this.element.title = newTitle 81 | } 82 | 83 | // eslint-disable-next-line accessor-pairs 84 | private set className (className: TagClasses) { 85 | this.element.className = className 86 | } 87 | 88 | // eslint-disable-next-line accessor-pairs 89 | private set textContent (content: typeof this.element.textContent) { 90 | this.element.textContent = content 91 | } 92 | } 93 | 94 | export type { TagClasses, TagContent } 95 | export { Tag } 96 | -------------------------------------------------------------------------------- /src/TagStatus.ts: -------------------------------------------------------------------------------- 1 | enum TagStatus { 2 | empty = '', 3 | pending = '🕑', 4 | successful = '✅', 5 | caution = '⚠️', 6 | failed = '❌', 7 | global = '🌍', 8 | asterisk = '*', 9 | } 10 | 11 | export { TagStatus } 12 | -------------------------------------------------------------------------------- /src/Trustless.ts: -------------------------------------------------------------------------------- 1 | import fetchPonyfill from 'fetch-ponyfill' 2 | import { CheckBase } from './CheckBase' 3 | import { Log } from './Log' 4 | import { HASH_TO_TEST, TRUSTLESS_RESPONSE_TYPES } from './constants' 5 | import type { GatewayNode } from './GatewayNode' 6 | import type { Checkable } from './types' 7 | 8 | const { fetch } = fetchPonyfill() 9 | 10 | const log = new Log('Trustless') 11 | 12 | class Trustless extends CheckBase implements Checkable { 13 | _className = 'Trustless' 14 | _tagName = 'div' 15 | constructor (protected parent: GatewayNode) { 16 | super(parent, 'div', 'Trustless') 17 | } 18 | 19 | async check (): Promise { 20 | const now = Date.now() 21 | const gatewayAndHash = `${this.parent.gateway}/ipfs/${HASH_TO_TEST}` 22 | this.parent.tag.classList.add('trustless') 23 | try { 24 | const trustlessResponseTypesTests = await Promise.all(TRUSTLESS_RESPONSE_TYPES.map( 25 | async (trustlessTypes): Promise => { 26 | const testUrl = `${gatewayAndHash}?format=${trustlessTypes}&now=${now}#x-ipfs-companion-no-redirect` 27 | const response = await fetch(testUrl) 28 | return Boolean(response.headers.get('Content-Type')?.includes(`application/vnd.ipld.${trustlessTypes}`)) 29 | }, 30 | )) 31 | 32 | const failedTests = TRUSTLESS_RESPONSE_TYPES.filter((_result, idx) => !trustlessResponseTypesTests[idx]) 33 | 34 | if (failedTests.length === 0) { 35 | this.tag.win() 36 | } else { 37 | const errorMsg = `URL '${gatewayAndHash} does not support the following Trustless response types: [` + 38 | `${failedTests.join(', ')}]` 39 | 40 | log.debug(errorMsg) 41 | throw new Error(errorMsg) 42 | } 43 | } catch (err) { 44 | log.error(err) 45 | this.onerror() 46 | throw err 47 | } 48 | } 49 | 50 | checked (): void { 51 | log.warn('Not implemented yet') 52 | } 53 | 54 | onerror (): void { 55 | this.tag.err() 56 | } 57 | } 58 | 59 | export { Trustless } 60 | -------------------------------------------------------------------------------- /src/UiComponent.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from './Tag' 2 | import type { Checker } from './Checker' 3 | import type { GatewayNode } from './GatewayNode' 4 | import type { Results } from './Results' 5 | import type { Visible } from './types' 6 | 7 | class UiComponent { 8 | tag: Tag 9 | constructor (protected parent: Visible | Checker | GatewayNode | Results, ...tagParams: ConstructorParameters) { 10 | this.tag = new Tag(...tagParams) 11 | } 12 | } 13 | 14 | export { UiComponent } 15 | -------------------------------------------------------------------------------- /src/checkViaImgSrc.ts: -------------------------------------------------------------------------------- 1 | // import { Log } from './Log' 2 | 3 | // const log = new Log('checkViaImgSrc') 4 | 5 | async function checkViaImgSrc (imgUrl: string | URL): Promise { 6 | // we check if gateway is up by loading 1x1 px image: 7 | // this is more robust check than loading js, as it won't be blocked 8 | // by privacy protections present in modern browsers or in extensions such as Privacy Badger 9 | const imgCheckTimeout = 15000 10 | await new Promise((resolve, reject) => { 11 | const img = new Image() 12 | const timer: ReturnType = setTimeout(() => { 13 | // clearTimeout(timer) 14 | reject(new Error(`Timeout when attempting to load img from '${img.src}`)) 15 | }, imgCheckTimeout) 16 | // const timeout = () => { 17 | // if (timer == null) { 18 | // return false 19 | // } 20 | // clearTimeout(timer) 21 | // // timer = null 22 | // return true 23 | // } 24 | const onImageError: OnErrorEventHandler = (event, source, lineno, colno, error) => { 25 | clearTimeout(timer) 26 | 27 | if (error == null) { 28 | reject(new Error(`Unknown Error when attempting to load img from '${img.src}`)) 29 | } else { 30 | reject(error) 31 | } 32 | } 33 | img.onerror = onImageError 34 | img.onload = () => { 35 | // subdomain works 36 | // timeout() 37 | clearTimeout(timer) 38 | resolve() 39 | } 40 | img.src = imgUrl.toString() 41 | }) 42 | } 43 | 44 | export { checkViaImgSrc } 45 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const HASH_STRING = 'Hello from IPFS Gateway Checker' 2 | const HASH_TO_TEST = 'bafybeifx7yeb55armcsxwwitkymga5xf53dxiarykms3ygqic223w5sk3m' 3 | const IMG_HASH = 'bafybeibwzifw52ttrkqlikfzext5akxu7lz4xiwjgwzmqcpdzmp3n5vnbe' // 1x1.png 4 | // const IFRAME_HASH = 'bafkreifx3g6bkkwl7b4v43lvcqfo5vshbiehuvmpky2zayhfpg5qj7y3ca' 5 | const IPNS_PATH_TO_TEST = '/ipns/en.wikipedia-on-ipfs.org/favicon.ico' 6 | const TRUSTLESS_RESPONSE_TYPES = ['raw', 'car'] 7 | const DEFAULT_IPFS_GATEWAY = 'https://ipfs.io' 8 | 9 | export { 10 | DEFAULT_IPFS_GATEWAY, 11 | HASH_STRING, 12 | HASH_TO_TEST, 13 | // IFRAME_HASH, 14 | IMG_HASH, 15 | IPNS_PATH_TO_TEST, 16 | TRUSTLESS_RESPONSE_TYPES, 17 | } 18 | -------------------------------------------------------------------------------- /src/expectSubdomainRedirect.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url-ponyfill' 2 | import { Log } from './Log' 3 | import { IMG_HASH } from './constants' 4 | 5 | const log = new Log('expectSubdomainRedirect') 6 | 7 | async function expectSubdomainRedirect (url: string | URL): Promise { 8 | // Detecting redirects on remote Origins is extra tricky, 9 | // but we seem to be able to access xhr.responseURL which is enough to see 10 | // if paths are redirected to subdomains. 11 | 12 | const { redirected, url: responseUrl } = await fetch(url.toString()) 13 | log.debug('redirected: ', redirected) 14 | log.debug('responseUrl: ', responseUrl) 15 | 16 | if (redirected) { 17 | log.debug('definitely redirected') 18 | } 19 | const { hostname } = new URL(responseUrl) 20 | 21 | if (!hostname.startsWith(IMG_HASH)) { 22 | const msg = `Expected ${url.toString()} to redirect to subdomain '${IMG_HASH}' but instead received '${responseUrl}'` 23 | log.debug(msg) 24 | throw new Error(msg) 25 | } 26 | } 27 | 28 | export { expectSubdomainRedirect } 29 | -------------------------------------------------------------------------------- /src/gatewayHostname.ts: -------------------------------------------------------------------------------- 1 | import { HASH_TO_TEST } from './constants' 2 | 3 | function gatewayHostname (url: URL): string { 4 | let urlString: string = url.toString() 5 | 6 | if (url?.hostname != null) { 7 | urlString = url.hostname.toString() 8 | } 9 | 10 | return urlString.replace(`${HASH_TO_TEST}.ipfs.`, '') // skip .ipfs. in subdomain gateways 11 | .replace(`${HASH_TO_TEST}.`, '') // path-based 12 | } 13 | 14 | export { gatewayHostname } 15 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@dutu/rate-limiter' 2 | declare module 'ipfs-geoip' 3 | 4 | declare namespace IpfsGeoip { 5 | interface LookupResponse { 6 | country_code: string 7 | country_name: string 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Public Gateway Checker | IPFS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | IPFS 21 | 22 |
23 |
24 |
25 |

Public Gateways

26 | 33 |
34 |
35 | 36 |
37 |
38 |

SECURITY NOTES

39 |
    40 |
  • Avoid storing sensitive data (or providing credentials) on websites loaded via gateways, especially ones marked with ⚠️
    41 | These are legacy gateways for fetching standalone data, not designed to serve dapps/websites.
    42 | They do NOT provide origin isolation and trustless, verifiable retrieval. 43 |
  • 44 |
  • 45 | The list contains gateways operated by various parties, coordinated by loose mutual consensus, without a central governing authority.
    46 | IPFS Foundation operates and is responsible for only three public good gateways: ipfs.io, dweb.link and trustless-gateway.link. 47 |
  • 48 |
49 |
50 |
51 |
52 |
53 |
Online
54 | 55 | 56 | 57 | 58 |
Country
59 | 60 |
ΔT
61 |
62 |
63 | 64 | 110 | 111 | 123 |
124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Workbox } from 'workbox-window' 2 | import gateways from '../gateways.json' 3 | import { Checker } from './Checker' 4 | import { Log } from './Log' 5 | import { loadMetrics } from './metrics' 6 | 7 | const wb = new Workbox('/sw.js') 8 | void wb.register() 9 | 10 | // note: currently disabled and `.metricsConsentToggle` is currently display:none; 11 | loadMetrics() 12 | const log = new Log('App index') 13 | 14 | window.checker = new Checker() 15 | 16 | window.checker.checkGateways(gateways).catch((err) => { 17 | log.error('Unexpected error') 18 | log.error(err) 19 | }) 20 | -------------------------------------------------------------------------------- /src/metrics.ts: -------------------------------------------------------------------------------- 1 | const metricsConsent = localStorage.getItem('metrics_consent') 2 | 3 | const metricsNotificationModal = document.querySelector('.js-metrics-notification-modal') 4 | const metricsAgreementContent = document.querySelector('.js-metrics-agreement') 5 | const metricsManagePreferencesContent = document.querySelector('.js-metrics-preferences') 6 | 7 | const closeNotificationModalX = document.querySelector('.js-modal-close') 8 | const confirmMetricNoticeBtn = document.querySelector('.js-metrics-notification-confirm') 9 | 10 | const saveMetricPreferencesBtn = document.querySelector('.js-metrics-notification-preferences-save') 11 | 12 | const managePreferencesBtn = document.querySelector('.js-metrics-notification-manage') 13 | const necessaryMetricsToggle = document.querySelector('.js-necessary-toggle') as HTMLInputElement 14 | const metricsModalToggle = document.querySelector('.js-metrics-modal-toggle') 15 | 16 | function addConsent (consent: string[]): void { 17 | // TODO: add consent to metrics provider 18 | 19 | if (Array.isArray(consent)) { 20 | localStorage.setItem('metrics_consent', JSON.stringify(consent)) 21 | } else { 22 | localStorage.setItem( 23 | 'metrics_consent', 24 | JSON.stringify([consent]), 25 | ) 26 | } 27 | } 28 | 29 | function addConsentEventHandler (): void { 30 | metricsNotificationModal?.classList.add('hidden') 31 | 32 | addConsent(['minimal']) 33 | } 34 | 35 | function updateNecessaryMetricPreferences (): void { 36 | const necessaryMetricsAccepted = necessaryMetricsToggle.checked 37 | 38 | if (necessaryMetricsAccepted) { 39 | addConsent(['minimal']) 40 | } else { 41 | // TODO: remove consent from metrics provider 42 | localStorage.setItem('metrics_consent', JSON.stringify([])) 43 | } 44 | } 45 | 46 | function initMetricsModal (): void { 47 | metricsNotificationModal?.classList.remove('hidden') 48 | confirmMetricNoticeBtn?.classList.remove('hidden') 49 | managePreferencesBtn?.classList.remove('hidden') 50 | metricsAgreementContent?.classList.remove('hidden') 51 | closeNotificationModalX?.addEventListener('click', hideMetricsModal) 52 | confirmMetricNoticeBtn?.addEventListener('click', addConsentEventHandler) 53 | managePreferencesBtn?.addEventListener('click', managePreferencesClicked) 54 | } 55 | 56 | function hideMetricsModal (): void { 57 | metricsNotificationModal?.classList.add('hidden') 58 | metricsManagePreferencesContent?.classList.add('hidden') 59 | metricsAgreementContent?.classList.remove('hidden') 60 | } 61 | 62 | function managePreferencesClicked (): void { 63 | const metricsConsent = localStorage.getItem('metrics_consent') 64 | if (metricsConsent != null) necessaryMetricsToggle.checked = JSON.parse(metricsConsent)[0] === 'minimal' 65 | metricsAgreementContent?.classList.add('hidden') 66 | saveMetricPreferencesBtn?.classList.remove('hidden') 67 | metricsManagePreferencesContent?.classList.remove('hidden') 68 | 69 | necessaryMetricsToggle.addEventListener('click', updateNecessaryMetricPreferences) 70 | saveMetricPreferencesBtn?.addEventListener('click', hideMetricsModal) 71 | } 72 | 73 | function metricsModalToggleEventHandler (): void { 74 | initMetricsModal() 75 | } 76 | 77 | function loadMetrics (): void { 78 | metricsModalToggle?.addEventListener('click', metricsModalToggleEventHandler) 79 | // TODO: initialize metrics provider 80 | 81 | if (metricsConsent != null) { 82 | addConsent(JSON.parse(metricsConsent)) 83 | } else { 84 | addConsent(['minimal']) 85 | } 86 | } 87 | 88 | export { loadMetrics } 89 | -------------------------------------------------------------------------------- /src/report.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://4everland.io": { 3 | "metadata": { 4 | "time": "2024-04-22T18:10:43.119150092Z", 5 | "version": "v0.5.1", 6 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 7 | "gateway_url": "https://4everland.io" 8 | }, 9 | "results": { 10 | "DNSLink": { 11 | "pass": 0, 12 | "fail": 1, 13 | "skip": 0 14 | }, 15 | "CORS": { 16 | "pass": 0, 17 | "fail": 1, 18 | "skip": 0 19 | }, 20 | "JSON-CBOR": { 21 | "pass": 0, 22 | "fail": 3, 23 | "skip": 0 24 | } 25 | } 26 | }, 27 | "https://cf-ipfs.com": { 28 | "metadata": { 29 | "time": "2024-04-22T18:10:48.514912661Z", 30 | "version": "v0.5.1", 31 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 32 | "gateway_url": "https://cf-ipfs.com" 33 | }, 34 | "results": { 35 | "DNSLink": { 36 | "pass": 0, 37 | "fail": 1, 38 | "skip": 0 39 | }, 40 | "CORS": { 41 | "pass": 0, 42 | "fail": 1, 43 | "skip": 0 44 | }, 45 | "JSON-CBOR": { 46 | "pass": 0, 47 | "fail": 3, 48 | "skip": 0 49 | } 50 | } 51 | }, 52 | "https://cloudflare-ipfs.com": { 53 | "metadata": { 54 | "time": "2024-04-22T18:10:49.189135087Z", 55 | "version": "v0.5.1", 56 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 57 | "gateway_url": "https://cloudflare-ipfs.com" 58 | }, 59 | "results": { 60 | "DNSLink": { 61 | "pass": 0, 62 | "fail": 1, 63 | "skip": 0 64 | }, 65 | "CORS": { 66 | "pass": 0, 67 | "fail": 1, 68 | "skip": 0 69 | }, 70 | "JSON-CBOR": { 71 | "pass": 1, 72 | "fail": 3, 73 | "skip": 0 74 | } 75 | } 76 | }, 77 | "https://dweb.link": { 78 | "metadata": { 79 | "time": "2024-04-22T18:10:46.060969525Z", 80 | "version": "v0.5.1", 81 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 82 | "gateway_url": "https://dweb.link" 83 | }, 84 | "results": { 85 | "DNSLink": { 86 | "pass": 0, 87 | "fail": 1, 88 | "skip": 0 89 | }, 90 | "CORS": { 91 | "pass": 1, 92 | "fail": 0, 93 | "skip": 0 94 | }, 95 | "JSON-CBOR": { 96 | "pass": 0, 97 | "fail": 3, 98 | "skip": 0 99 | } 100 | } 101 | }, 102 | "https://gateway.pinata.cloud": { 103 | "metadata": { 104 | "time": "2024-04-22T18:10:48.073788394Z", 105 | "version": "v0.5.1", 106 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 107 | "gateway_url": "https://gateway.pinata.cloud" 108 | }, 109 | "results": { 110 | "DNSLink": { 111 | "pass": 0, 112 | "fail": 1, 113 | "skip": 0 114 | }, 115 | "CORS": { 116 | "pass": 0, 117 | "fail": 1, 118 | "skip": 0 119 | }, 120 | "JSON-CBOR": { 121 | "pass": 1, 122 | "fail": 3, 123 | "skip": 0 124 | } 125 | } 126 | }, 127 | "https://hardbin.com": { 128 | "metadata": { 129 | "time": "2024-04-22T18:11:14.16941472Z", 130 | "version": "v0.5.1", 131 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 132 | "gateway_url": "https://hardbin.com" 133 | }, 134 | "results": { 135 | "DNSLink": { 136 | "pass": 0, 137 | "fail": 1, 138 | "skip": 0 139 | }, 140 | "CORS": { 141 | "pass": 0, 142 | "fail": 1, 143 | "skip": 0 144 | }, 145 | "JSON-CBOR": { 146 | "pass": 0, 147 | "fail": 3, 148 | "skip": 0 149 | } 150 | } 151 | }, 152 | "https://ipfs.eth.aragon.network": { 153 | "metadata": { 154 | "time": "2024-04-22T18:11:49.206634363Z", 155 | "version": "v0.5.1", 156 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 157 | "gateway_url": "https://ipfs.eth.aragon.network" 158 | }, 159 | "results": { 160 | "DNSLink": { 161 | "pass": 0, 162 | "fail": 1, 163 | "skip": 0 164 | }, 165 | "CORS": { 166 | "pass": 0, 167 | "fail": 1, 168 | "skip": 0 169 | }, 170 | "JSON-CBOR": { 171 | "pass": 3, 172 | "fail": 1, 173 | "skip": 0 174 | } 175 | } 176 | }, 177 | "https://ipfs.io": { 178 | "metadata": { 179 | "time": "2024-04-22T18:17:47.998018002Z", 180 | "version": "v0.5.1", 181 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 182 | "gateway_url": "https://ipfs.io" 183 | }, 184 | "results": { 185 | "DNSLink": { 186 | "pass": 0, 187 | "fail": 1, 188 | "skip": 0 189 | }, 190 | "CORS": { 191 | "pass": 1, 192 | "fail": 0, 193 | "skip": 0 194 | }, 195 | "JSON-CBOR": { 196 | "pass": 0, 197 | "fail": 3, 198 | "skip": 0 199 | } 200 | } 201 | }, 202 | "https://ipfs.runfission.com": { 203 | "metadata": { 204 | "time": "2024-04-22T18:12:36.200555879Z", 205 | "version": "v0.5.1", 206 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 207 | "gateway_url": "https://ipfs.runfission.com" 208 | }, 209 | "results": { 210 | "DNSLink": { 211 | "pass": 0, 212 | "fail": 2, 213 | "skip": 0 214 | }, 215 | "CORS": { 216 | "pass": 0, 217 | "fail": 1, 218 | "skip": 0 219 | }, 220 | "JSON-CBOR": { 221 | "pass": 4, 222 | "fail": 1, 223 | "skip": 0 224 | }, 225 | "IPNS": { 226 | "pass": 2, 227 | "fail": 2, 228 | "skip": 0 229 | }, 230 | "Others": { 231 | "pass": 3, 232 | "fail": 3, 233 | "skip": 0 234 | }, 235 | "Block-CAR": { 236 | "pass": 3, 237 | "fail": 6, 238 | "skip": 0 239 | }, 240 | "Tar": { 241 | "pass": 1, 242 | "fail": 0, 243 | "skip": 0 244 | }, 245 | "UnixFS": { 246 | "pass": 2, 247 | "fail": 1, 248 | "skip": 0 249 | }, 250 | "Subdomains": { 251 | "pass": 0, 252 | "fail": 3, 253 | "skip": 0 254 | } 255 | } 256 | }, 257 | "https://nftstorage.link": { 258 | "metadata": { 259 | "time": "2024-04-22T18:10:45.605956127Z", 260 | "version": "v0.5.1", 261 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 262 | "gateway_url": "https://nftstorage.link" 263 | }, 264 | "results": { 265 | "DNSLink": { 266 | "pass": 0, 267 | "fail": 1, 268 | "skip": 0 269 | }, 270 | "CORS": { 271 | "pass": 0, 272 | "fail": 1, 273 | "skip": 0 274 | }, 275 | "JSON-CBOR": { 276 | "pass": 0, 277 | "fail": 3, 278 | "skip": 0 279 | } 280 | } 281 | }, 282 | "https://storry.tv": { 283 | "metadata": { 284 | "time": "2024-04-22T18:10:51.035643056Z", 285 | "version": "v0.5.1", 286 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 287 | "gateway_url": "https://storry.tv" 288 | }, 289 | "results": { 290 | "DNSLink": { 291 | "pass": 0, 292 | "fail": 1, 293 | "skip": 0 294 | }, 295 | "CORS": { 296 | "pass": 0, 297 | "fail": 1, 298 | "skip": 0 299 | }, 300 | "JSON-CBOR": { 301 | "pass": 0, 302 | "fail": 3, 303 | "skip": 0 304 | } 305 | } 306 | }, 307 | "https://trustless-gateway.link": { 308 | "metadata": { 309 | "time": "2024-04-22T18:10:56.420186716Z", 310 | "version": "v0.5.1", 311 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 312 | "gateway_url": "https://trustless-gateway.link" 313 | }, 314 | "results": { 315 | "DNSLink": { 316 | "pass": 0, 317 | "fail": 1, 318 | "skip": 0 319 | }, 320 | "CORS": { 321 | "pass": 0, 322 | "fail": 1, 323 | "skip": 0 324 | }, 325 | "JSON-CBOR": { 326 | "pass": 0, 327 | "fail": 3, 328 | "skip": 0 329 | } 330 | } 331 | }, 332 | "https://w3s.link": { 333 | "metadata": { 334 | "time": "2024-04-22T18:10:45.079602511Z", 335 | "version": "v0.5.1", 336 | "job_url": "https://github.com/ipfs/public-gateway-checker/actions/runs/8788832024", 337 | "gateway_url": "https://w3s.link" 338 | }, 339 | "results": { 340 | "DNSLink": { 341 | "pass": 0, 342 | "fail": 1, 343 | "skip": 0 344 | }, 345 | "CORS": { 346 | "pass": 0, 347 | "fail": 1, 348 | "skip": 0 349 | }, 350 | "JSON-CBOR": { 351 | "pass": 0, 352 | "fail": 3, 353 | "skip": 0 354 | } 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | box-sizing: border-box; 4 | text-align: center; 5 | } 6 | 7 | div.Stats { 8 | display: flex; 9 | justify-content: flex-start; 10 | align-items: center; 11 | margin: 1em auto; 12 | width: 95%; 13 | max-width: 1024px; 14 | } 15 | 16 | div.Stats div.Totals, 17 | div.Stats div.Gateways { 18 | border-radius: 9999px; 19 | color: white; 20 | font-weight: bold; 21 | padding: 0.5em 1em; 22 | margin: 0 1em 0 0; 23 | font-family: Consolas, monaco, monospace; 24 | } 25 | 26 | div.Stats div.Gateways { 27 | background-color: #0b3a53; 28 | } 29 | 30 | div.Stats div.Totals { 31 | background-color: #0cb892; 32 | } 33 | 34 | div.Results { 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: center; 38 | margin: 0 auto; 39 | width: 100%; 40 | max-width: 1024px; 41 | } 42 | 43 | div.Node { 44 | display: flex; 45 | justify-content: space-evenly; 46 | align-items: center; 47 | border-bottom: 1px solid #edf0f4; 48 | padding: 0.5em 1em; 49 | } 50 | 51 | div.Node:hover { 52 | background-color: #edf0f4; 53 | } 54 | 55 | 56 | div.Node div.Link { 57 | width: 100%; 58 | text-align: left; 59 | padding-left: 2em; 60 | white-space: nowrap; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | } 64 | 65 | div.Node a, 66 | div#origin-warning a { 67 | text-decoration: none; 68 | color: #357edd; 69 | white-space: nowrap; 70 | } 71 | 72 | div.Node div.Status, 73 | div.Node div.Cors, 74 | div.Node div.Origin, 75 | div.Node div.Trustless, 76 | div.Node div.Ipns, 77 | div.Node div.Flag { 78 | width: 7.2em; 79 | text-align: center; 80 | margin: 0 0.5em; 81 | user-select: none; 82 | } 83 | 84 | div.Node div.Flag { 85 | /* width: 2em; */ 86 | height: 1em; 87 | background-repeat: no-repeat; 88 | background-size: contain; 89 | background-position: center; 90 | display: inline-block 91 | } 92 | 93 | div.Node div.Took { 94 | min-width: 5em; 95 | text-align: right; 96 | font-size: 80%; 97 | font-style: italic; 98 | } 99 | 100 | div.Node.trustless div.Trustless { 101 | margin: 0 1.3em; 102 | } 103 | 104 | div.Node:not(.online):not(:first-child) { 105 | opacity: .5 106 | } 107 | 108 | div#origin-warning { 109 | padding: .1rem 1.5rem; 110 | border-left: 4px solid #e7c000; 111 | background-color: #fff7d2; 112 | color: #34373f; 113 | } 114 | 115 | div#origin-warning p { 116 | letter-spacing: .5px; 117 | color: #b29400; 118 | font-weight: 600; 119 | margin-bottom: -.4rem; 120 | } 121 | 122 | div#origin-warning ol, 123 | div#origin-warning p, 124 | div#origin-warning ul { 125 | line-height: 1.7; 126 | } 127 | 128 | 129 | div#checker\.results .Node:nth-child(1) .Link { 130 | padding-left: 1em; 131 | } 132 | 133 | /** 134 | * Consent notification for metrics collection 135 | * Should be a color that is not too bright, but still visible 136 | */ 137 | div.metrics-notification-wrapper { 138 | display: flex; 139 | flex-direction: column; 140 | justify-content: center; 141 | align-items: center; 142 | max-width: 480px; 143 | width: 100%; 144 | position: fixed; 145 | bottom: 12px; 146 | left: 20px; 147 | z-index: 2; 148 | background: #ffffff; 149 | border-radius: 4px; 150 | border-top: 6px solid #3B8C90; 151 | box-shadow: 0px 8px 14px rgba(77, 72, 69, 0.15), 152 | 0px 1px 4px rgba(77, 72, 69, 0.15); 153 | padding: 28px 40px; 154 | } 155 | 156 | div.metrics-notification-wrapper div.metrics-modal-close { 157 | position: absolute; 158 | font-size: 20px; 159 | top: 5px; 160 | right: 5px; 161 | font-weight: 100; 162 | color: #073A53; 163 | height: 30px; 164 | width: 30px; 165 | display: flex; 166 | align-items: center; 167 | justify-content: center; 168 | cursor: pointer; 169 | } 170 | 171 | div.metrics-notification-wrapper h3.metrics-notification-heading { 172 | font-size: 24px; 173 | line-height: 29px; 174 | font-weight: 300; 175 | position: relative; 176 | padding-bottom: 12px; 177 | color: #073A53; 178 | font-style: normal; 179 | margin: 0; 180 | } 181 | 182 | div.metrics-notification-wrapper h3.metrics-notification-heading::after { 183 | content: ''; 184 | position: absolute; 185 | left: 0; 186 | bottom: 0; 187 | width: 80px; 188 | height: 4px; 189 | background-color: #3B8C90; 190 | border-radius: 2px; 191 | 192 | } 193 | 194 | div.metrics-notification-container { 195 | width: 100%; 196 | display: flex; 197 | flex-direction: column; 198 | align-items: flex-start; 199 | justify-content: center; 200 | } 201 | 202 | .hidden { 203 | display: none !important; 204 | } 205 | 206 | div.metrics-notification-wrapper .metrics-notification-text { 207 | text-align: left; 208 | color: #073A53; 209 | font-size: 16px; 210 | font-weight: 300; 211 | margin-top: 16px; 212 | line-height: 20px; 213 | width: 100%; 214 | } 215 | 216 | .metrics-notification-text .metrics-notification-customize-text { 217 | padding-left: 10px; 218 | } 219 | 220 | div.metrics-notification-wrapper .metrics-notification-customize { 221 | width: 100%; 222 | display: flex; 223 | flex-direction: column; 224 | align-items: center; 225 | justify-content: center; 226 | } 227 | 228 | div.metrics-notification-wrapper .metrics-notification-buttons { 229 | display: flex; 230 | justify-content: space-between; 231 | align-items: center; 232 | margin-top: 20px; 233 | width: 100%; 234 | max-width: 315px; 235 | } 236 | 237 | div.metrics-notification-wrapper .metrics-notification-content-preferences .metrics-notification-buttons { 238 | justify-content: center; 239 | max-width: 100%; 240 | } 241 | 242 | div.metrics-notification-wrapper a { 243 | /* color: rgba(107, 196, 206, 1); */ 244 | color: #073A53; 245 | } 246 | 247 | div.metrics-notification-wrapper .metrics-notification-buttons button { 248 | border: none; 249 | color: #3B8C90; 250 | cursor: pointer; 251 | background-color: transparent; 252 | display: flex; 253 | align-items: center; 254 | justify-content: center; 255 | border-radius: 2px; 256 | font-weight: 400; 257 | font-size: 16px; 258 | padding: 11px 20px; 259 | } 260 | 261 | .metrics-notification-wrapper .metrics-notification-buttons button.primary { 262 | background-color: #3B8C90; 263 | color: rgba(246, 248, 251, 1); 264 | font-weight: 600; 265 | } 266 | 267 | .metrics-notification-wrapper .metrics-notification-buttons button#metrics-notification-preferences-save { 268 | width: 215px; 269 | } 270 | 271 | 272 | .metrics-notification-content { 273 | display: flex; 274 | width: 100%; 275 | flex-direction: column; 276 | justify-content: center; 277 | align-items: flex-start; 278 | } 279 | 280 | button#metrics-notification-warning-close { 281 | border: 1pt solid rgba(107, 196, 206, 1); 282 | /* color: rgba(7, 58, 83, 1); */ 283 | } 284 | 285 | .metrics-notification-preference-list { 286 | margin: 0; 287 | padding: 20px 0 0; 288 | width: 100%; 289 | display: flex; 290 | align-items: center; 291 | justify-content: center; 292 | } 293 | 294 | .metrics-notification-preference-list .metrics-notification-preference-list-item { 295 | width: 100%; 296 | height: 40px; 297 | background: #F5F7FD; 298 | color: #073A53; 299 | display: flex; 300 | align-items: center; 301 | justify-content: space-between; 302 | padding: 0 10px; 303 | margin: 0; 304 | border-radius: 8px; 305 | } 306 | 307 | .metrics-notification-preference-list .preference-list-item-switch { 308 | position: relative; 309 | display: inline-block; 310 | width: 50px; 311 | height: 22px; 312 | } 313 | 314 | .metrics-notification-preference-list .preference-list-item-switch input { 315 | width: 0; 316 | height: 0; 317 | opacity: 0; 318 | } 319 | 320 | .metrics-notification-preference-list .preference-list-item-switch .toggle-slider { 321 | position: absolute; 322 | cursor: pointer; 323 | top: 0; 324 | left: 0; 325 | right: 0; 326 | bottom: 0; 327 | background-color: #C4C7D6; 328 | -webkit-transition: .4s; 329 | transition: .4s; 330 | border-radius: 50px; 331 | } 332 | 333 | .metrics-notification-preference-list .preference-list-item-switch .toggle-slider:before { 334 | position: absolute; 335 | content: ""; 336 | height: 18px; 337 | width: 18px; 338 | left: 1px; 339 | bottom: 2px; 340 | background: linear-gradient(180deg, #FFFFFF 0%, #E8EAEA 100%); 341 | -webkit-transition: .4s; 342 | transition: .4s; 343 | border-radius: 50%; 344 | } 345 | 346 | .metrics-notification-preference-list .preference-list-item-switch input:checked+.toggle-slider { 347 | background-color: #3B8C90; 348 | } 349 | 350 | .metrics-notification-preference-list .preference-list-item-switch input:checked+.toggle-slider:before { 351 | -webkit-transform: translateX(29px); 352 | -ms-transform: translateX(29px); 353 | transform: translateX(29px); 354 | } 355 | 356 | @media (max-width: 768px) { 357 | div.metrics-notification-wrapper { 358 | left: 0; 359 | bottom: 0; 360 | } 361 | 362 | .metrics-notification-wrapper .metrics-notification-text { 363 | font-size: 15px; 364 | } 365 | 366 | div.metrics-notification-wrapper .metrics-notification-buttons { 367 | font-size: 15px; 368 | } 369 | 370 | div.metrics-notification-wrapper h3.metrics-notification-heading { 371 | font-size: 20px; 372 | } 373 | } 374 | 375 | @media (max-width: 480px) { 376 | div.metrics-notification-wrapper { 377 | left: 0; 378 | bottom: 0; 379 | padding: 20px; 380 | border-top: 3px solid #3B8C90; 381 | } 382 | 383 | div.metrics-notification-wrapper div.metrics-modal-close { 384 | top: 4px; 385 | right: 4px; 386 | } 387 | } 388 | 389 | 390 | /** 391 | * From https://unpkg.com/@beyonk/gdpr-cookie-consent-banner@9.1.0/dist/style.css 392 | */ 393 | .metricsConsentToggle { 394 | /* TODO: make visible when we implement other metrics */ 395 | display: none; 396 | width: 43px; 397 | height: 43px; 398 | will-change: transform; 399 | padding: 0; 400 | border: 0; 401 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 402 | background: white; 403 | border-radius: 50%; 404 | transition: 200ms; 405 | opacity: 1; 406 | z-index: 1; 407 | cursor: pointer; 408 | margin: 0; 409 | position: fixed; 410 | bottom: 12px; 411 | left: 20px; 412 | } 413 | 414 | .metricsConsentToggle:disabled { 415 | cursor: default; 416 | } 417 | 418 | .metricsConsentToggle:hover { 419 | fill: #60BCC7 !important; 420 | } 421 | 422 | .metricsConsentToggle:hover rect, 423 | .metricsConsentToggle:hover circle:not(:first-child) { 424 | fill: #60BCC7 !important; 425 | } 426 | 427 | .metricsConsentToggle:hover path { 428 | stroke: #60BCC7 !important; 429 | } 430 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Checker } from './Checker' 2 | import type { Tag } from './Tag' 3 | 4 | declare global { 5 | interface Window { 6 | checker: Checker 7 | } 8 | } 9 | /** 10 | * An interface that allows various properties for gateways to be checked 11 | */ 12 | export interface Checkable { 13 | 14 | // @todo: Update to async/await 15 | // check: () => Promise 16 | check(): void 17 | checked(): void 18 | onerror(): void 19 | } 20 | 21 | /** 22 | * A class implementing the Visible interface supports functionality that can make it visible in the UI 23 | */ 24 | export interface Visible { 25 | tag: Tag 26 | _tagName: string 27 | _className: string 28 | } 29 | 30 | export interface DnsQueryResponseAnswer { name: string, type: number, TTL: number, data: string } 31 | export interface DnsQueryResponseQuestion { name: string, type: number } 32 | 33 | export interface DnsQueryResponseAuthority { 34 | TTL: number 35 | data: string // "aragorn.ns.cloudflare.com. dns.cloudflare.com. 2271826322 10000 2400 604800 3600" 36 | name: string // "stibarc.com" 37 | type: number 38 | } 39 | 40 | export interface DnsQueryResponse { 41 | AD: boolean 42 | Answer?: DnsQueryResponseAnswer[] 43 | Authority?: DnsQueryResponseAuthority[] 44 | CD: boolean 45 | Question: DnsQueryResponseQuestion[] 46 | RA: boolean 47 | RD: boolean 48 | Status: number 49 | TC: boolean 50 | } 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "resolveJsonModule": true, 6 | "outDir": "dist", 7 | "paths": { 8 | "*": ["./types/*"] 9 | } 10 | }, 11 | "include": [ 12 | "types", 13 | "test", // remove this line if you don't want to type-check tests 14 | "src", 15 | "src/report.json", 16 | "gateways.json" 17 | ], 18 | "ts-node": { 19 | "transpileOnly": true, 20 | "compilerOptions": { 21 | "module": "CommonJS" 22 | } 23 | } 24 | } 25 | --------------------------------------------------------------------------------