diff --git a/.husky/pre-commit b/.husky/pre-commit index 2cbae3d..c345039 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -7,3 +7,4 @@ npm test npx lint-staged npm run checkLicense +npm run checkSecrets diff --git a/README.md b/README.md index ce1db76..ee9c6a5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Copyright (C) 2023 Shusui Moyatani AGPL-3.0-or-later +Please note that some files are licensed in other FLOSS licenses. +Those files contain the license text in the first lines. +If the file does not have such text, it must be considered licensed in AGPL. + ### English This program is free software: you can redistribute it and/or modify diff --git a/package.json b/package.json index f417687..5f424e9 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "cover": "vitest run --coverage", "prepare": "husky install", "generatePackageInfo": "node -e 'import(\"./scripts/generatePackageInfo.mjs\").then((m) => m.default())'", - "checkLicense": "node -e 'import(\"./scripts/checkLicense.mjs\").then((m) => m.default())'" + "checkLicense": "node -e 'import(\"./scripts/checkLicense.mjs\").then((m) => m.default())'", + "checkSecrets": "node -e 'import(\"./scripts/checkSecrets.mjs\").then((m) => m.default())'" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.59.7", diff --git a/scripts/checkSecrets.mjs b/scripts/checkSecrets.mjs new file mode 100644 index 0000000..b0838b7 --- /dev/null +++ b/scripts/checkSecrets.mjs @@ -0,0 +1,82 @@ +/** + * To the extent possible under law, the person who associated CC0 + * with this file has waived all copyright and related or + * neighboring rights to this work. + * + * https://creativecommons.org/publicdomain/zero/1.0/ + */ +import fs from 'fs/promises'; +import path from 'path'; +import util from 'util'; + +const rootDir = path.resolve(); +const pattern = /nsec1[0-9a-zA-Z]+/; +const ignored = [/^node_modules$/, /^\./, /\.tsbuildinfo$/, /^public$/, /^dist$/]; + +const ignoreNextLine = /@check-secrets-disable-next-line/; + +const shouldIgnore = (filename) => ignored.some((pattern) => pattern.test(filename)); + +const searchFiles = async (folderPath) => { + let didMatch = false; + + const files = await fs.readdir(folderPath); + + for (const file of files) { + const filePath = path.join(folderPath, file); + const stats = await fs.stat(filePath); + + if (shouldIgnore(file)) continue; + + if (stats.isDirectory()) { + const match = await searchFiles(filePath); + didMatch ||= match; + } else { + const match = await checkKeyword(filePath); + didMatch ||= match; + } + } + + return didMatch; +}; + +const checkKeyword = async (filePath) => { + const content = await fs.readFile(filePath, 'utf8'); + const lines = content.split('\n'); + let prevLine = ''; + let didMatch = false; + let didShowFilename = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + const match = line.match(pattern); + if (match != null) { + if (!didShowFilename) { + console.error(filePath); + didShowFilename = true; + } + if (ignoreNextLine.test(prevLine.trim())) { + console.error(`ignored: ${i + 1}: "${match[0]}": ${line}`); + continue; + } + console.error(`${i + 1}: "${match[0]}": ${line}`); + didMatch = true; + } + + prevLine = line; + } + + return didMatch; +}; + +const main = async () => { + const result = await searchFiles(rootDir); + if (result) { + process.exit(1); + } else { + process.exit(0); + } +}; + +export default main;