├── .env.example ├── .github └── workflows │ └── backup.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NOTION_TOKEN=Notion Auth Token 2 | NOTION_SPACE_ID=Notion Space ID 3 | NOTION_USER_ID=Notion User ID 4 | -------------------------------------------------------------------------------- /.github/workflows/backup.yml: -------------------------------------------------------------------------------- 1 | name: "Backup Notion Workspace" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | backup: 12 | runs-on: ubuntu-latest 13 | name: Backup 14 | timeout-minutes: 15 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: '20' 20 | 21 | - name: Run backup script 22 | run: npm install && npm run backup 23 | env: 24 | NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} 25 | NOTION_SPACE_ID: ${{ secrets.NOTION_SPACE_ID }} 26 | NOTION_USER_ID: ${{ secrets.NOTION_USER_ID }} 27 | 28 | - name: Checkout backup repo 29 | uses: actions/checkout@v4 30 | with: 31 | repository: ${{ secrets.REPO_USERNAME }}/${{ secrets.REPO_NAME }} 32 | token: ${{ secrets.REPO_PERSONAL_ACCESS_TOKEN }} 33 | path: "backup-repo" 34 | 35 | - name: Commit changes to backup repo 36 | env: 37 | REPO_EMAIL: ${{ secrets.REPO_EMAIL }} 38 | REPO_USERNAME: ${{ secrets.REPO_USERNAME }} 39 | run: | 40 | cp -a workspace/. backup-repo/ 41 | cd backup-repo 42 | git config --local user.email "${REPO_EMAIL}" 43 | git config --local user.name "${REPO_USERNAME}" 44 | git add . 45 | if [ -z "$(git status --porcelain)" ]; then 46 | exit 0 47 | fi 48 | git commit -m "Automated Notion workspace backup" 49 | git push 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | workspace/ 3 | workspace.zip 4 | .env 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Richard Keil 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notion Guardian 2 | 3 | A tool that automatically backups your [Notion](notion.so) workspace and commits changes to another repository. 4 | 5 | Notion Guardian offers a quick way to setup a secure backup of your data in a private repository — allowing you to track how your notes change over time and to know that your data is safe. 6 | 7 | The tool separates the logic for running the export and the actual workspace data into two repositories. This way your backups are not cluttered with other scripts. If you prefer to have a one-repo solution or want to backup specific blocks of your workspace, checkout the [notion-backup fork by upleveled](https://github.com/upleveled/notion-backup). 8 | 9 | ## How to setup 10 | 11 | 1. Create a separate private repository for your backups to live in (e.g. "my-notion-backup"). Make sure you create a `main` branch — for example by clicking "Add a README file" when creating the repo. 12 | 2. Use this repository ("notion-guardian") as a template in order to create a copy (Click the green "Use this template" button). 13 | 3. Create a Personal Access Token ([docs](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)) with the "repo" scope and store it as `REPO_PERSONAL_ACCESS_TOKEN` in the secrets of the copied repo. 14 | 4. Store your GitHub username in the `REPO_USERNAME` secret. 15 | 5. Store the name of your newly created private repo in the `REPO_NAME` secret (in this case "my-notion-backup"). 16 | 6. Store the email that should be used to commit changes (usually your GitHub account email) in the `REPO_EMAIL` secret. 17 | 7. Obtain your Notion space-id and token as described [in this Medium post](https://medium.com/@arturburtsev/automated-notion-backups-f6af4edc298d). Store it in the `NOTION_SPACE_ID` and `NOTION_TOKEN` secret. 18 | 8. You will also need to obtain your `notion_user_id` the same way and store it in a `NOTION_USER_ID` secret. 19 | 9. Wait until the action runs for the first time or push a commit to the repo to trigger the first backup. 20 | 10. Check your private repo to see that an automatic commit with your Notion workspace data has been made. Done 🙌 21 | 22 | ## How it works 23 | 24 | This repo contains a GitHub workflow that runs every day and for every push to this repo. The workflow will execute the script which makes an export request to Notion, waits for it to finish and downloads the workspace content to a temporary directory. The workflow will then commit this directory to the repository configured in the repo secrets. 25 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import axios from "axios"; 3 | import AdmZip from "adm-zip"; 4 | import { createWriteStream, promises as fs } from "fs"; 5 | import { join } from "path"; 6 | 7 | type NotionTask = { 8 | id: string; 9 | state: string; 10 | status: { 11 | pagesExported: number; 12 | exportURL: string; 13 | }; 14 | error?: string; 15 | }; 16 | 17 | const { NOTION_TOKEN, NOTION_SPACE_ID, NOTION_USER_ID } = process.env; 18 | if (!NOTION_TOKEN || !NOTION_SPACE_ID || !NOTION_USER_ID) { 19 | throw new Error( 20 | "Environment variable NOTION_TOKEN, NOTION_SPACE_ID or NOTION_USER_ID is missing. Check the README.md for more information." 21 | ); 22 | } 23 | 24 | const client = axios.create({ 25 | baseURL: "https://www.notion.so/api/v3", // Unofficial Notion API 26 | headers: { 27 | Cookie: `token_v2=${NOTION_TOKEN};`, 28 | "x-notion-active-user-header": NOTION_USER_ID, 29 | }, 30 | }); 31 | 32 | const sleep = async (seconds: number): Promise => { 33 | return new Promise((resolve) => { 34 | setTimeout(resolve, seconds * 1000); 35 | }); 36 | }; 37 | 38 | const round = (number: number) => Math.round(number * 100) / 100; 39 | 40 | const exportFromNotion = async ( 41 | destination: string, 42 | format: string 43 | ): Promise => { 44 | const task = { 45 | eventName: "exportSpace", 46 | request: { 47 | spaceId: NOTION_SPACE_ID, 48 | shouldExportComments: false, 49 | exportOptions: { 50 | exportType: format, 51 | collectionViewExportType: "currentView", 52 | timeZone: "Europe/Berlin", 53 | locale: "en", 54 | preferredViewMap: {}, 55 | }, 56 | }, 57 | }; 58 | const { 59 | data: { taskId }, 60 | }: { data: { taskId: string } } = await client.post("enqueueTask", { task }); 61 | 62 | console.log(`Started export as task [${taskId}].`); 63 | 64 | let exportURL: string; 65 | let fileTokenCookie: string | undefined; 66 | let retries = 0; 67 | 68 | while (true) { 69 | await sleep(2 ** retries); // Exponential backoff 70 | try { 71 | const { 72 | data: { results: tasks }, 73 | headers: { "set-cookie": getTasksRequestCookies }, 74 | }: { 75 | data: { results: NotionTask[] }; 76 | headers: { [key: string]: string[] }; 77 | } = await client.post("getTasks", { taskIds: [taskId] }); 78 | const task = tasks.find((t) => t.id === taskId); 79 | 80 | if (!task) throw new Error(`Task [${taskId}] not found.`); 81 | if (task.error) throw new Error(`Export failed with reason: ${task.error}`); 82 | 83 | console.log(`Exported ${task.status.pagesExported} pages.`); 84 | 85 | if (task.state === "success") { 86 | exportURL = task.status.exportURL; 87 | fileTokenCookie = getTasksRequestCookies.find((cookie) => 88 | cookie.includes("file_token=") 89 | ); 90 | if (!fileTokenCookie) { 91 | throw new Error("Task finished but file_token cookie not found."); 92 | } 93 | console.log(`Export finished.`); 94 | break; 95 | } 96 | 97 | // Reset retries on success 98 | retries = 0; 99 | } catch (error) { 100 | if (axios.isAxiosError(error) && error.response?.status === 429) { 101 | console.log("Received response with HTTP 429 (Too Many Requests), retrying after backoff."); 102 | retries += 1; 103 | continue; 104 | } 105 | 106 | // Rethrow if it's not a 429 error 107 | throw error; 108 | } 109 | } 110 | 111 | const response = await client({ 112 | method: "GET", 113 | url: exportURL, 114 | responseType: "stream", 115 | headers: { Cookie: fileTokenCookie }, 116 | }); 117 | 118 | const size = response.headers["content-length"]; 119 | console.log(`Downloading ${round(size / 1000 / 1000)}mb...`); 120 | 121 | const stream = response.data.pipe(createWriteStream(destination)); 122 | await new Promise((resolve, reject) => { 123 | stream.on("close", resolve); 124 | stream.on("error", reject); 125 | }); 126 | }; 127 | 128 | const extractZip = async ( 129 | filename: string, 130 | destination: string 131 | ): Promise => { 132 | const zip = new AdmZip(filename); 133 | zip.extractAllTo(destination, true); 134 | 135 | const extractedFiles = zip.getEntries().map((entry) => entry.entryName); 136 | const partFiles = extractedFiles.filter((name) => 137 | name.match(/Part-\d+\.zip/) 138 | ); 139 | 140 | // Extract found "Part-*.zip" files to destination and delete them: 141 | await Promise.all( 142 | partFiles.map(async (partFile: string) => { 143 | partFile = join(destination, partFile); 144 | const partZip = new AdmZip(partFile); 145 | partZip.extractAllTo(destination, true); 146 | await fs.unlink(partFile); 147 | }) 148 | ); 149 | 150 | const extractedFolders = await fs.readdir(destination); 151 | const exportFolders = extractedFolders.filter((name: string) => 152 | name.startsWith("Export-") 153 | ); 154 | 155 | // Move the contents of found "Export-*" folders to destination and delete them: 156 | await Promise.all( 157 | exportFolders.map(async (folderName: string) => { 158 | const folderPath = join(destination, folderName); 159 | const contents = await fs.readdir(folderPath); 160 | await Promise.all( 161 | contents.map(async (file: string) => { 162 | const filePath = join(folderPath, file); 163 | const newFilePath = join(destination, file); 164 | await fs.rename(filePath, newFilePath); 165 | }) 166 | ); 167 | await fs.rmdir(folderPath); 168 | }) 169 | ); 170 | }; 171 | 172 | const run = async (): Promise => { 173 | const workspaceDir = join(process.cwd(), "workspace"); 174 | const workspaceZip = join(process.cwd(), "workspace.zip"); 175 | 176 | await exportFromNotion(workspaceZip, "markdown"); 177 | await fs.rm(workspaceDir, { recursive: true, force: true }); 178 | await fs.mkdir(workspaceDir, { recursive: true }); 179 | await extractZip(workspaceZip, workspaceDir); 180 | await fs.unlink(workspaceZip); 181 | 182 | console.log("✅ Export downloaded and unzipped."); 183 | }; 184 | 185 | run(); 186 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-guardian", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "notion-guardian", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "adm-zip": "^0.5.10", 13 | "axios": "^1.7.2", 14 | "dotenv": "^16.3.1" 15 | }, 16 | "devDependencies": { 17 | "@tsconfig/node20": "^20.1.2", 18 | "@types/adm-zip": "^0.5.3", 19 | "ts-node": "^10.9.1", 20 | "typescript": "^5.2.2" 21 | } 22 | }, 23 | "node_modules/@cspotcode/source-map-support": { 24 | "version": "0.8.1", 25 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 26 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 27 | "dev": true, 28 | "dependencies": { 29 | "@jridgewell/trace-mapping": "0.3.9" 30 | }, 31 | "engines": { 32 | "node": ">=12" 33 | } 34 | }, 35 | "node_modules/@jridgewell/resolve-uri": { 36 | "version": "3.1.1", 37 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", 38 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", 39 | "dev": true, 40 | "engines": { 41 | "node": ">=6.0.0" 42 | } 43 | }, 44 | "node_modules/@jridgewell/sourcemap-codec": { 45 | "version": "1.4.15", 46 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 47 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 48 | "dev": true 49 | }, 50 | "node_modules/@jridgewell/trace-mapping": { 51 | "version": "0.3.9", 52 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 53 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 54 | "dev": true, 55 | "dependencies": { 56 | "@jridgewell/resolve-uri": "^3.0.3", 57 | "@jridgewell/sourcemap-codec": "^1.4.10" 58 | } 59 | }, 60 | "node_modules/@tsconfig/node10": { 61 | "version": "1.0.9", 62 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 63 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 64 | "dev": true 65 | }, 66 | "node_modules/@tsconfig/node12": { 67 | "version": "1.0.11", 68 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 69 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 70 | "dev": true 71 | }, 72 | "node_modules/@tsconfig/node14": { 73 | "version": "1.0.3", 74 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 75 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 76 | "dev": true 77 | }, 78 | "node_modules/@tsconfig/node16": { 79 | "version": "1.0.4", 80 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 81 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 82 | "dev": true 83 | }, 84 | "node_modules/@tsconfig/node20": { 85 | "version": "20.1.2", 86 | "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.2.tgz", 87 | "integrity": "sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==", 88 | "dev": true 89 | }, 90 | "node_modules/@types/adm-zip": { 91 | "version": "0.5.5", 92 | "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.5.tgz", 93 | "integrity": "sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==", 94 | "dev": true, 95 | "dependencies": { 96 | "@types/node": "*" 97 | } 98 | }, 99 | "node_modules/@types/node": { 100 | "version": "20.8.9", 101 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", 102 | "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", 103 | "dev": true, 104 | "dependencies": { 105 | "undici-types": "~5.26.4" 106 | } 107 | }, 108 | "node_modules/acorn": { 109 | "version": "8.11.2", 110 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", 111 | "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", 112 | "dev": true, 113 | "bin": { 114 | "acorn": "bin/acorn" 115 | }, 116 | "engines": { 117 | "node": ">=0.4.0" 118 | } 119 | }, 120 | "node_modules/acorn-walk": { 121 | "version": "8.3.0", 122 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", 123 | "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", 124 | "dev": true, 125 | "engines": { 126 | "node": ">=0.4.0" 127 | } 128 | }, 129 | "node_modules/adm-zip": { 130 | "version": "0.5.10", 131 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", 132 | "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", 133 | "engines": { 134 | "node": ">=6.0" 135 | } 136 | }, 137 | "node_modules/arg": { 138 | "version": "4.1.3", 139 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 140 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 141 | "dev": true 142 | }, 143 | "node_modules/asynckit": { 144 | "version": "0.4.0", 145 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 146 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 147 | }, 148 | "node_modules/axios": { 149 | "version": "1.7.2", 150 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", 151 | "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", 152 | "dependencies": { 153 | "follow-redirects": "^1.15.6", 154 | "form-data": "^4.0.0", 155 | "proxy-from-env": "^1.1.0" 156 | } 157 | }, 158 | "node_modules/combined-stream": { 159 | "version": "1.0.8", 160 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 161 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 162 | "dependencies": { 163 | "delayed-stream": "~1.0.0" 164 | }, 165 | "engines": { 166 | "node": ">= 0.8" 167 | } 168 | }, 169 | "node_modules/create-require": { 170 | "version": "1.1.1", 171 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 172 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 173 | "dev": true 174 | }, 175 | "node_modules/delayed-stream": { 176 | "version": "1.0.0", 177 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 178 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 179 | "engines": { 180 | "node": ">=0.4.0" 181 | } 182 | }, 183 | "node_modules/diff": { 184 | "version": "4.0.2", 185 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 186 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 187 | "dev": true, 188 | "engines": { 189 | "node": ">=0.3.1" 190 | } 191 | }, 192 | "node_modules/dotenv": { 193 | "version": "16.4.5", 194 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 195 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 196 | "engines": { 197 | "node": ">=12" 198 | }, 199 | "funding": { 200 | "url": "https://dotenvx.com" 201 | } 202 | }, 203 | "node_modules/follow-redirects": { 204 | "version": "1.15.6", 205 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 206 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 207 | "funding": [ 208 | { 209 | "type": "individual", 210 | "url": "https://github.com/sponsors/RubenVerborgh" 211 | } 212 | ], 213 | "engines": { 214 | "node": ">=4.0" 215 | }, 216 | "peerDependenciesMeta": { 217 | "debug": { 218 | "optional": true 219 | } 220 | } 221 | }, 222 | "node_modules/form-data": { 223 | "version": "4.0.0", 224 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 225 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 226 | "dependencies": { 227 | "asynckit": "^0.4.0", 228 | "combined-stream": "^1.0.8", 229 | "mime-types": "^2.1.12" 230 | }, 231 | "engines": { 232 | "node": ">= 6" 233 | } 234 | }, 235 | "node_modules/make-error": { 236 | "version": "1.3.6", 237 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 238 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 239 | "dev": true 240 | }, 241 | "node_modules/mime-db": { 242 | "version": "1.52.0", 243 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 244 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 245 | "engines": { 246 | "node": ">= 0.6" 247 | } 248 | }, 249 | "node_modules/mime-types": { 250 | "version": "2.1.35", 251 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 252 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 253 | "dependencies": { 254 | "mime-db": "1.52.0" 255 | }, 256 | "engines": { 257 | "node": ">= 0.6" 258 | } 259 | }, 260 | "node_modules/proxy-from-env": { 261 | "version": "1.1.0", 262 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 263 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 264 | }, 265 | "node_modules/ts-node": { 266 | "version": "10.9.1", 267 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", 268 | "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", 269 | "dev": true, 270 | "dependencies": { 271 | "@cspotcode/source-map-support": "^0.8.0", 272 | "@tsconfig/node10": "^1.0.7", 273 | "@tsconfig/node12": "^1.0.7", 274 | "@tsconfig/node14": "^1.0.0", 275 | "@tsconfig/node16": "^1.0.2", 276 | "acorn": "^8.4.1", 277 | "acorn-walk": "^8.1.1", 278 | "arg": "^4.1.0", 279 | "create-require": "^1.1.0", 280 | "diff": "^4.0.1", 281 | "make-error": "^1.1.1", 282 | "v8-compile-cache-lib": "^3.0.1", 283 | "yn": "3.1.1" 284 | }, 285 | "bin": { 286 | "ts-node": "dist/bin.js", 287 | "ts-node-cwd": "dist/bin-cwd.js", 288 | "ts-node-esm": "dist/bin-esm.js", 289 | "ts-node-script": "dist/bin-script.js", 290 | "ts-node-transpile-only": "dist/bin-transpile.js", 291 | "ts-script": "dist/bin-script-deprecated.js" 292 | }, 293 | "peerDependencies": { 294 | "@swc/core": ">=1.2.50", 295 | "@swc/wasm": ">=1.2.50", 296 | "@types/node": "*", 297 | "typescript": ">=2.7" 298 | }, 299 | "peerDependenciesMeta": { 300 | "@swc/core": { 301 | "optional": true 302 | }, 303 | "@swc/wasm": { 304 | "optional": true 305 | } 306 | } 307 | }, 308 | "node_modules/typescript": { 309 | "version": "5.2.2", 310 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 311 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 312 | "dev": true, 313 | "bin": { 314 | "tsc": "bin/tsc", 315 | "tsserver": "bin/tsserver" 316 | }, 317 | "engines": { 318 | "node": ">=14.17" 319 | } 320 | }, 321 | "node_modules/undici-types": { 322 | "version": "5.26.5", 323 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 324 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 325 | "dev": true 326 | }, 327 | "node_modules/v8-compile-cache-lib": { 328 | "version": "3.0.1", 329 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 330 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 331 | "dev": true 332 | }, 333 | "node_modules/yn": { 334 | "version": "3.1.1", 335 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 336 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 337 | "dev": true, 338 | "engines": { 339 | "node": ">=6" 340 | } 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-guardian", 3 | "version": "1.0.0", 4 | "description": "Keeps your Notion workspace safe and version controlled at all times.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "backup": "ts-node index.ts" 9 | }, 10 | "author": "Richard Keil", 11 | "license": "MIT", 12 | "dependencies": { 13 | "adm-zip": "^0.5.10", 14 | "axios": "^1.7.2", 15 | "dotenv": "^16.3.1" 16 | }, 17 | "devDependencies": { 18 | "@tsconfig/node20": "^20.1.2", 19 | "@types/adm-zip": "^0.5.3", 20 | "ts-node": "^10.9.1", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | adm-zip: 9 | specifier: ^0.5.10 10 | version: 0.5.10 11 | axios: 12 | specifier: ^0.21 13 | version: 0.21.4 14 | dotenv: 15 | specifier: ^16.3.1 16 | version: 16.3.1 17 | 18 | devDependencies: 19 | '@tsconfig/node20': 20 | specifier: ^20.1.2 21 | version: 20.1.2 22 | '@types/adm-zip': 23 | specifier: ^0.5.3 24 | version: 0.5.3 25 | ts-node: 26 | specifier: ^10.9.1 27 | version: 10.9.1(@types/node@20.8.9)(typescript@5.2.2) 28 | typescript: 29 | specifier: ^5.2.2 30 | version: 5.2.2 31 | 32 | packages: 33 | 34 | /@cspotcode/source-map-support@0.8.1: 35 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 36 | engines: {node: '>=12'} 37 | dependencies: 38 | '@jridgewell/trace-mapping': 0.3.9 39 | dev: true 40 | 41 | /@jridgewell/resolve-uri@3.1.1: 42 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 43 | engines: {node: '>=6.0.0'} 44 | dev: true 45 | 46 | /@jridgewell/sourcemap-codec@1.4.15: 47 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 48 | dev: true 49 | 50 | /@jridgewell/trace-mapping@0.3.9: 51 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 52 | dependencies: 53 | '@jridgewell/resolve-uri': 3.1.1 54 | '@jridgewell/sourcemap-codec': 1.4.15 55 | dev: true 56 | 57 | /@tsconfig/node10@1.0.9: 58 | resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} 59 | dev: true 60 | 61 | /@tsconfig/node12@1.0.11: 62 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 63 | dev: true 64 | 65 | /@tsconfig/node14@1.0.3: 66 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 67 | dev: true 68 | 69 | /@tsconfig/node16@1.0.4: 70 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 71 | dev: true 72 | 73 | /@tsconfig/node20@20.1.2: 74 | resolution: {integrity: sha512-madaWq2k+LYMEhmcp0fs+OGaLFk0OenpHa4gmI4VEmCKX4PJntQ6fnnGADVFrVkBj0wIdAlQnK/MrlYTHsa1gQ==} 75 | dev: true 76 | 77 | /@types/adm-zip@0.5.3: 78 | resolution: {integrity: sha512-LfeDIiFdvphelYY2aMWTyQBr5cTb1EL9Qcu19jFizdt2sL/jL+fy1fE8IgAKBFI5XfbGukaRDDM5PiJTrovAhA==} 79 | dependencies: 80 | '@types/node': 20.8.9 81 | dev: true 82 | 83 | /@types/node@20.8.9: 84 | resolution: {integrity: sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==} 85 | dependencies: 86 | undici-types: 5.26.5 87 | dev: true 88 | 89 | /acorn-walk@8.3.0: 90 | resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} 91 | engines: {node: '>=0.4.0'} 92 | dev: true 93 | 94 | /acorn@8.11.2: 95 | resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} 96 | engines: {node: '>=0.4.0'} 97 | hasBin: true 98 | dev: true 99 | 100 | /adm-zip@0.5.10: 101 | resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==} 102 | engines: {node: '>=6.0'} 103 | dev: false 104 | 105 | /arg@4.1.3: 106 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 107 | dev: true 108 | 109 | /axios@0.21.4: 110 | resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} 111 | dependencies: 112 | follow-redirects: 1.15.3 113 | transitivePeerDependencies: 114 | - debug 115 | dev: false 116 | 117 | /create-require@1.1.1: 118 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 119 | dev: true 120 | 121 | /diff@4.0.2: 122 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 123 | engines: {node: '>=0.3.1'} 124 | dev: true 125 | 126 | /dotenv@16.3.1: 127 | resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} 128 | engines: {node: '>=12'} 129 | dev: false 130 | 131 | /follow-redirects@1.15.3: 132 | resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} 133 | engines: {node: '>=4.0'} 134 | peerDependencies: 135 | debug: '*' 136 | peerDependenciesMeta: 137 | debug: 138 | optional: true 139 | dev: false 140 | 141 | /make-error@1.3.6: 142 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 143 | dev: true 144 | 145 | /ts-node@10.9.1(@types/node@20.8.9)(typescript@5.2.2): 146 | resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} 147 | hasBin: true 148 | peerDependencies: 149 | '@swc/core': '>=1.2.50' 150 | '@swc/wasm': '>=1.2.50' 151 | '@types/node': '*' 152 | typescript: '>=2.7' 153 | peerDependenciesMeta: 154 | '@swc/core': 155 | optional: true 156 | '@swc/wasm': 157 | optional: true 158 | dependencies: 159 | '@cspotcode/source-map-support': 0.8.1 160 | '@tsconfig/node10': 1.0.9 161 | '@tsconfig/node12': 1.0.11 162 | '@tsconfig/node14': 1.0.3 163 | '@tsconfig/node16': 1.0.4 164 | '@types/node': 20.8.9 165 | acorn: 8.11.2 166 | acorn-walk: 8.3.0 167 | arg: 4.1.3 168 | create-require: 1.1.1 169 | diff: 4.0.2 170 | make-error: 1.3.6 171 | typescript: 5.2.2 172 | v8-compile-cache-lib: 3.0.1 173 | yn: 3.1.1 174 | dev: true 175 | 176 | /typescript@5.2.2: 177 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 178 | engines: {node: '>=14.17'} 179 | hasBin: true 180 | dev: true 181 | 182 | /undici-types@5.26.5: 183 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 184 | dev: true 185 | 186 | /v8-compile-cache-lib@3.0.1: 187 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 188 | dev: true 189 | 190 | /yn@3.1.1: 191 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 192 | engines: {node: '>=6'} 193 | dev: true 194 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "ts-node": { 4 | // It is faster to skip typechecking. 5 | // Remove if you want ts-node to do typechecking. 6 | "transpileOnly": true, 7 | "files": true 8 | }, 9 | "compilerOptions": { 10 | "noImplicitAny": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------