├── .gitignore ├── src ├── utils │ ├── fileExt.ts │ └── versioning.ts ├── index.ts ├── legacy │ ├── constants.ts │ ├── validatePlatform.ts │ ├── checkPlatform.ts │ └── handler.ts ├── types.ts ├── getPlatform.ts ├── constants.ts ├── services │ └── github.ts └── handler.ts ├── .prettierrc ├── jestconfig.json ├── wrangler.toml ├── .github ├── workflows │ ├── deploy.yml │ └── codeql.yml └── dependabot.yml ├── tsconfig.json ├── test └── handler.test.ts ├── webpack.config.js ├── LICENSE ├── LICENSE_MIT ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | transpiled 4 | /.idea/ 5 | -------------------------------------------------------------------------------- /src/utils/fileExt.ts: -------------------------------------------------------------------------------- 1 | export function fileExt(filename: string): string { 2 | return filename.split('.').pop() || '' 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { handleRequest } from './handler' 2 | 3 | addEventListener('fetch', (event) => { 4 | event.respondWith(handleRequest(event.request)) 5 | }) 6 | -------------------------------------------------------------------------------- /src/legacy/constants.ts: -------------------------------------------------------------------------------- 1 | export enum LEGACY_AVAILABLE_PLATFORMS { 2 | MacOS = 'darwin', 3 | Win32 = 'win32', 4 | Win64 = 'win64', 5 | Linux = 'linux', 6 | } 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type TauriUpdateResponse = { 2 | url: string 3 | version: string 4 | notes?: string 5 | pub_date?: string 6 | signature?: string 7 | } 8 | -------------------------------------------------------------------------------- /jestconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | "testRegex": "/test/.*\\.test\\.ts$", 6 | "collectCoverageFrom": ["src/**/*.{ts,js}"] 7 | } 8 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "tauri-update-server" 2 | workers_dev = true 3 | compatibility_date = "2022-03-26" 4 | main = "src/index.ts" 5 | 6 | [vars] 7 | GITHUB_ACCOUNT = 'killeencode' 8 | GITHUB_REPO = 'brancato' 9 | GITHUB_TOKEN = '' # Optional - used with private repos 10 | 11 | [build] 12 | command = "npm install && npm run build" 13 | -------------------------------------------------------------------------------- /src/utils/versioning.ts: -------------------------------------------------------------------------------- 1 | import semverValid from 'semver/functions/valid' 2 | import semverGt from 'semver/functions/gt' 3 | 4 | export function sanitizeVersion(version: string): string | undefined { 5 | // Works with or without v in version 6 | const semanticV = version.split('v').pop() 7 | return semanticV 8 | } 9 | 10 | export { semverGt } 11 | export { semverValid } 12 | -------------------------------------------------------------------------------- /src/legacy/validatePlatform.ts: -------------------------------------------------------------------------------- 1 | import { LEGACY_AVAILABLE_PLATFORMS } from './constants' 2 | 3 | export function validatePlatform(platform: string): string | undefined { 4 | switch (platform) { 5 | case LEGACY_AVAILABLE_PLATFORMS.MacOS: 6 | case LEGACY_AVAILABLE_PLATFORMS.Win32: 7 | case LEGACY_AVAILABLE_PLATFORMS.Win64: 8 | case LEGACY_AVAILABLE_PLATFORMS.Linux: 9 | return platform 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | repository_dispatch: 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | name: Deploy 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Publish 15 | uses: cloudflare/wrangler-action@1.3.0 16 | with: 17 | apiToken: ${{ secrets.CF_API_TOKEN }} 18 | env: 19 | CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "esnext", 6 | "lib": ["esnext"], 7 | "alwaysStrict": true, 8 | "strict": true, 9 | "preserveConstEnums": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "types": [ 14 | "@cloudflare/workers-types", 15 | "@types/jest", 16 | "@types/service-worker-mock" 17 | ] 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules", "dist", "test"] 21 | } 22 | -------------------------------------------------------------------------------- /test/handler.test.ts: -------------------------------------------------------------------------------- 1 | import { handleRequest } from '../src/handler' 2 | import makeServiceWorkerEnv from 'service-worker-mock' 3 | 4 | declare var global: any 5 | 6 | describe('handle', () => { 7 | beforeEach(() => { 8 | Object.assign(global, makeServiceWorkerEnv()) 9 | jest.resetModules() 10 | }) 11 | 12 | test('handle GET', async () => { 13 | const result = await handleRequest(new Request('/', { method: 'GET' })) 14 | expect(result.status).toEqual(200) 15 | const text = await result.text() 16 | expect(text).toEqual('request method: GET') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | output: { 6 | filename: 'worker.js', 7 | path: path.join(__dirname, 'dist'), 8 | }, 9 | devtool: 'cheap-module-source-map', 10 | mode: 'development', 11 | resolve: { 12 | extensions: ['.ts', '.tsx', '.js'], 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | loader: 'ts-loader', 19 | options: { 20 | // transpileOnly is useful to skip typescript checks occasionally: 21 | // transpileOnly: true, 22 | }, 23 | }, 24 | ], 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/getPlatform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ARCH_FILTERS, 3 | AVAILABLE_ARCHITECTURES, 4 | AVAILABLE_PLATFORMS, 5 | PLATFORM_FILTERS, 6 | } from './constants' 7 | import { fileExt } from './utils/fileExt' 8 | 9 | export const testAsset = ( 10 | target: AVAILABLE_PLATFORMS, 11 | arch: AVAILABLE_ARCHITECTURES, 12 | fileName: string, 13 | ): boolean => { 14 | const { matches, extension } = PLATFORM_FILTERS[target] 15 | const arch_matches = ARCH_FILTERS[arch] 16 | const rightArch = 17 | arch_matches && ARCH_FILTERS[arch].some((arch) => fileName.includes(arch)) 18 | 19 | // .app gz files don't have arch in the name 20 | if (!rightArch && target !== AVAILABLE_PLATFORMS.MacOS) { 21 | return false 22 | } 23 | 24 | if (fileExt(fileName) !== extension) { 25 | return false 26 | } 27 | 28 | return matches.some((match) => fileName.includes(match)) 29 | } 30 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum AVAILABLE_PLATFORMS { 2 | MacOS = 'darwin', 3 | Windows = 'windows', 4 | Linux = 'linux', 5 | } 6 | 7 | export enum AVAILABLE_ARCHITECTURES { 8 | x64 = 'x86_64', 9 | x86 = 'i686', 10 | arm64 = 'aarch64', 11 | armv7 = 'armv7', 12 | } 13 | 14 | //TODO: check if this is correct 15 | export const ARCH_FILTERS = { 16 | [AVAILABLE_ARCHITECTURES.x64]: ['x64', 'amd64'], 17 | [AVAILABLE_ARCHITECTURES.x86]: ['x86'], 18 | [AVAILABLE_ARCHITECTURES.arm64]: ['arm64'], 19 | [AVAILABLE_ARCHITECTURES.armv7]: ['armv7'], 20 | } as { [key in AVAILABLE_ARCHITECTURES]: string[] } 21 | 22 | export type Filter = { 23 | extension: string 24 | matches: string[] 25 | } 26 | export const PLATFORM_FILTERS = { 27 | [AVAILABLE_PLATFORMS.MacOS]: { 28 | extension: 'gz', 29 | matches: ['.app', 'osx'], 30 | }, 31 | [AVAILABLE_PLATFORMS.Windows]: { 32 | extension: 'zip', 33 | matches: ['x64', 'x32'], 34 | }, 35 | [AVAILABLE_PLATFORMS.Linux]: { 36 | extension: 'gz', 37 | matches: ['AppImage'], 38 | }, 39 | } as { 40 | [key in AVAILABLE_PLATFORMS]: Filter 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 KilleenCode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE_MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Cloudflare, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/legacy/checkPlatform.ts: -------------------------------------------------------------------------------- 1 | import { LEGACY_AVAILABLE_PLATFORMS } from './constants' 2 | import { fileExt } from '../utils/fileExt' 3 | 4 | export function checkPlatform( 5 | platform: string, 6 | fileName: string, 7 | ): string | undefined { 8 | const extension = fileExt(fileName) 9 | // OSX we should have our .app tar.gz 10 | if ( 11 | (fileName.includes('.app') || 12 | fileName.includes('darwin') || 13 | fileName.includes('osx')) && 14 | extension === 'gz' && 15 | platform === LEGACY_AVAILABLE_PLATFORMS.MacOS 16 | ) { 17 | return 'darwin' 18 | } 19 | 20 | // Windows 64 bits 21 | if ( 22 | (fileName.includes('x64') || fileName.includes('win64')) && 23 | extension === 'zip' && 24 | platform === LEGACY_AVAILABLE_PLATFORMS.Win64 25 | ) { 26 | return 'win64' 27 | } 28 | 29 | // Windows 32 bits 30 | if ( 31 | (fileName.includes('x32') || fileName.includes('win32')) && 32 | extension === 'zip' && 33 | platform === LEGACY_AVAILABLE_PLATFORMS.Win32 34 | ) { 35 | return 'win32' 36 | } 37 | 38 | // Linux app image 39 | if ( 40 | fileName.includes('AppImage') && 41 | extension === 'gz' && 42 | platform === LEGACY_AVAILABLE_PLATFORMS.Linux 43 | ) { 44 | return 'linux' 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker-typescript-template", 3 | "version": "1.0.0", 4 | "description": "Cloudflare worker TypeScript template", 5 | "main": "dist/worker.js", 6 | "scripts": { 7 | "dev": "wrangler dev", 8 | "build": "webpack", 9 | "format": "prettier --write '*.{json,js}' 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'", 10 | "lint": "eslint --max-warnings=0 src && prettier --check '*.{json,js}' 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'", 11 | "test": "jest --config jestconfig.json --verbose" 12 | }, 13 | "author": "author", 14 | "license": "MIT OR Apache-2.0", 15 | "eslintConfig": { 16 | "root": true, 17 | "extends": [ 18 | "typescript", 19 | "prettier" 20 | ] 21 | }, 22 | "devDependencies": { 23 | "@cloudflare/workers-types": "^3.0.0", 24 | "@types/jest": "^26.0.23", 25 | "@types/semver": "^7.3.9", 26 | "@types/service-worker-mock": "^2.0.1", 27 | "@typescript-eslint/eslint-plugin": "^4.16.1", 28 | "@typescript-eslint/parser": "^4.16.1", 29 | "eslint": "^7.21.0", 30 | "eslint-config-prettier": "^8.1.0", 31 | "eslint-config-typescript": "^3.0.0", 32 | "jest": "^27.0.1", 33 | "prettier": "^2.3.0", 34 | "service-worker-mock": "^2.0.5", 35 | "ts-jest": "^27.0.1", 36 | "ts-loader": "^9.2.2", 37 | "typescript": "^4.3.2", 38 | "webpack": "^5.38.1", 39 | "webpack-cli": "^4.7.0", 40 | "wrangler": "^2.6.2" 41 | }, 42 | "dependencies": { 43 | "semver": "^7.3.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/github.ts: -------------------------------------------------------------------------------- 1 | export type Asset = { name: string; browser_download_url: string } 2 | export const getReleases = async (request: Request): Promise => { 3 | const reqUrl = new URL( 4 | `https://api.github.com/repos/${GITHUB_ACCOUNT}/${GITHUB_REPO}/releases/latest`, 5 | ) 6 | const headers = new Headers({ 7 | Accept: 'application/vnd.github.preview', 8 | 'User-Agent': request.headers.get('User-Agent') as string, 9 | }) 10 | 11 | if (GITHUB_TOKEN?.length) headers.set('Authorization', `token ${GITHUB_TOKEN}`) 12 | 13 | return await fetch(reqUrl.toString(), { 14 | method: 'GET', 15 | headers, 16 | }) 17 | } 18 | 19 | type Release = { 20 | tag_name: string 21 | assets: Asset[] 22 | body: string 23 | published_at: string 24 | } 25 | 26 | export const getLatestRelease = async (request: Request): Promise => { 27 | const releases = await getReleases(request) 28 | 29 | return (await releases.json()) as Release 30 | } 31 | 32 | export async function findAssetSignature( 33 | fileName: string, 34 | assets: Asset[], 35 | ): Promise { 36 | // check in our assets if we have a file: `fileName.sig` 37 | // by example fileName can be: App-1.0.0.zip 38 | 39 | const foundSignature = assets.find( 40 | (asset) => asset.name.toLowerCase() === `${fileName.toLowerCase()}.sig`, 41 | ) 42 | 43 | if (!foundSignature) { 44 | return undefined 45 | } 46 | 47 | const response = await fetch(foundSignature.browser_download_url) 48 | if (response.status !== 200) { 49 | return undefined 50 | } 51 | const signature = await response.text() 52 | return signature 53 | } 54 | -------------------------------------------------------------------------------- /src/legacy/handler.ts: -------------------------------------------------------------------------------- 1 | import { validatePlatform } from './validatePlatform' 2 | import { checkPlatform } from './checkPlatform' 3 | import { Asset, findAssetSignature, getReleases } from '../services/github' 4 | import { TauriUpdateResponse } from '../types' 5 | import { sanitizeVersion, semverGt, semverValid } from '../utils/versioning' 6 | 7 | export const handleLegacyRequest = async ( 8 | request: Request, 9 | ): Promise => { 10 | const path = new URL(request.url).pathname 11 | const [platform, version] = path.slice(1).split('/') 12 | 13 | const releases = await getReleases(request) 14 | 15 | const release = (await releases.clone().json()) as { 16 | tag_name: string 17 | assets: Asset[] 18 | body: string 19 | published_at: string 20 | } 21 | if (!platform || !validatePlatform(platform) || !version) { 22 | return releases 23 | } 24 | const remoteVersion = sanitizeVersion(release.tag_name.toLowerCase()) 25 | 26 | if (!remoteVersion || !semverValid(remoteVersion)) { 27 | return new Response('Not found', { status: 404 }) 28 | } 29 | 30 | const shouldUpdate = semverGt(remoteVersion, version) 31 | if (!shouldUpdate) { 32 | return new Response(null, { status: 204 }) 33 | } 34 | 35 | for (const asset of release.assets) { 36 | const { name, browser_download_url } = asset 37 | const findPlatform = checkPlatform(platform, name) 38 | if (!findPlatform) { 39 | continue 40 | } 41 | 42 | // try to find signature for this asset 43 | const signature = await findAssetSignature(name, release.assets) 44 | const data: TauriUpdateResponse = { 45 | url: browser_download_url, 46 | version: remoteVersion, 47 | notes: release.body, 48 | pub_date: release.published_at, 49 | signature, 50 | } 51 | return new Response(JSON.stringify(data), { 52 | headers: { 'Content-Type': 'application/json; charset=utf-8' }, 53 | }) 54 | } 55 | 56 | return new Response(JSON.stringify({ remoteVersion, version, shouldUpdate })) 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED! 2 | Please see the actively maintained fork over at https://github.com/mackenly/tauri-update-cloudflare 3 | 4 | 5 | # Tauri Update Server: Cloudflare 6 | 7 | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/killeencode/tauri-update-cloudflare) 8 | 9 | ## One-Click Deploy 10 | 1. Click the button above, let Cloudflare walk you through: it's easy! 11 | 2. Go to your forked repository, edit `wrangler.toml`: 12 | - Update `GITHUB_ACCOUNT` and `GITHUB_REPO` to point to the Tauri project you're publishing releases from 13 | 14 | Much credit to [@lemarier](https://github.com/lemarier) for the underlying logic at https://github.com/lemarier/updater-deno 15 | 16 | ## Tauri Version Support 17 | ### Tauri >= v1.0.0-rc5: 18 | 19 | use `https://your-update-server.com/v1` route 20 | 21 | For example usage, see [Brancato config](https://github.com/KilleenCode/brancato/blob/main/src-tauri/tauri.conf.json#L55) 22 | 23 | ### Legacy 24 | use `https://your-update-server.com/` 25 | 26 | ## Cloudflare Wrangler 27 | 28 | ### 👩 💻 Developing 29 | 30 | `wrangler dev` 31 | 32 | [`src/index.ts`](./src/index.ts) calls the request handler in [`src/handler.ts`](./src/handler.ts), and will return the [request method](https://developer.mozilla.org/en-US/docs/Web/API/Request/method) for the given request. 33 | 34 | ### 🧪 Testing 35 | 36 | This template comes with jest tests which simply test that the request handler can handle each request method. `npm test` will run your tests. 37 | 38 | ### 👀 Previewing and Publishing 39 | 40 | `wrangler preview` 41 | `wrangler publish` 42 | 43 | For information on how to preview and publish your worker, please see the [Wrangler docs](https://developers.cloudflare.com/workers/tooling/wrangler/commands/#publish). 44 | 45 | 46 | ## Private repos 47 | 48 | In order to work with private repos you need to set `GITHUB_TOKEN` variable to your `wrangler.toml` file. You can create a [personal access token here](https://github.com/settings/tokens/new), create it with the repo permissions. 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '38 16 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /src/handler.ts: -------------------------------------------------------------------------------- 1 | import { testAsset } from './getPlatform' 2 | import semverValid from 'semver/functions/valid' 3 | import semverGt from 'semver/functions/gt' 4 | import { AVAILABLE_ARCHITECTURES, AVAILABLE_PLATFORMS } from './constants' 5 | import { handleLegacyRequest } from './legacy/handler' 6 | import { findAssetSignature, getLatestRelease } from './services/github' 7 | import { TauriUpdateResponse } from './types' 8 | import { sanitizeVersion } from './utils/versioning' 9 | 10 | declare global { 11 | const GITHUB_ACCOUNT: string 12 | const GITHUB_REPO: string 13 | const GITHUB_TOKEN: string 14 | } 15 | 16 | const SendJSON = (data: Record) => { 17 | return new Response(JSON.stringify(data), { 18 | headers: { 'Content-Type': 'application/json; charset=utf-8' }, 19 | }) 20 | } 21 | 22 | const responses = { 23 | NotFound: () => new Response('Not found', { status: 404 }), 24 | NoContent: () => new Response(null, { status: 204 }), 25 | SendUpdate: (data: TauriUpdateResponse) => 26 | SendJSON(data), 27 | SendJSON 28 | 29 | } 30 | 31 | type RequestPathParts = [ 32 | string, 33 | AVAILABLE_PLATFORMS, 34 | AVAILABLE_ARCHITECTURES, 35 | string, 36 | ] 37 | const handleV1Request = async (request: Request) => { 38 | const path = new URL(request.url).pathname 39 | const [, target, arch, appVersion] = path 40 | .slice(1) 41 | .split('/') as RequestPathParts 42 | 43 | if (!target || !arch || !appVersion || !semverValid(appVersion)) { 44 | return responses.NotFound() 45 | } 46 | const release = await getLatestRelease(request) 47 | 48 | const remoteVersion = sanitizeVersion(release.tag_name.toLowerCase()) 49 | if (!remoteVersion || !semverValid(remoteVersion)) { 50 | return responses.NotFound() 51 | } 52 | 53 | const shouldUpdate = semverGt(remoteVersion, appVersion) 54 | if (!shouldUpdate) { 55 | return responses.NoContent() 56 | } 57 | 58 | const match = release.assets.find(({ name }) => { 59 | const test = testAsset(target, arch, name) 60 | 61 | return test 62 | }) 63 | 64 | if (typeof match === 'undefined') { 65 | return responses.NotFound() 66 | } 67 | 68 | const signature = await findAssetSignature(match.name, release.assets) 69 | const proxy = GITHUB_TOKEN?.length; 70 | const downloadURL = proxy ? createProxiedFileUrl(match.browser_download_url, request) : match.browser_download_url 71 | const data: TauriUpdateResponse = { 72 | url: downloadURL, 73 | version: remoteVersion, 74 | notes: release.body, 75 | pub_date: release.published_at, 76 | signature, 77 | } 78 | 79 | return responses.SendUpdate(data) 80 | } 81 | 82 | const createProxiedFileUrl = (downloadURL: string, request: Request) => { 83 | 84 | const fileName = downloadURL.split('/')?.at(-1) 85 | if (!fileName) { throw new Error('Could not get file name from download URL') } 86 | 87 | 88 | const path = new URL(request.url) 89 | const root = `${path.protocol}//${path.host}` 90 | 91 | return new URL(`/latest/${fileName}`, root).toString() 92 | } 93 | 94 | const getLatestAssets = async (request: Request) => { 95 | const fileName = request.url.split('/')?.at(-1) 96 | if (!fileName) { throw new Error('Could not get file name from download URL') } 97 | 98 | const release = await getLatestRelease(request) 99 | const downloadPath = release.assets.find(({ name }) => name === fileName)?.browser_download_url 100 | 101 | if (!downloadPath) { throw new Error('Could not get file path from download URL') } 102 | 103 | const { readable, writable } = new TransformStream(); 104 | const file_response = await fetch(downloadPath, { 105 | method: 'GET', 106 | redirect: 'follow' 107 | }) 108 | 109 | file_response?.body?.pipeTo(writable); 110 | return new Response(readable, file_response); 111 | 112 | } 113 | 114 | export async function handleRequest(request: Request): Promise { 115 | const path = new URL(request.url).pathname 116 | 117 | 118 | if (path.includes('/latest')) { 119 | return getLatestAssets(request) 120 | } 121 | const version = path.slice(1).split('/')[0] 122 | 123 | if (version.includes('v')) { 124 | switch (version) { 125 | case 'v1': 126 | default: 127 | return handleV1Request(request) 128 | } 129 | } 130 | 131 | return handleLegacyRequest(request) 132 | } 133 | --------------------------------------------------------------------------------