From 3146add35a954834f867467dcc332c4f57727f46 Mon Sep 17 00:00:00 2001 From: Justin Ling <2521993+itsjling@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:18:38 +0000 Subject: [PATCH 1/4] Declare MIT license --- LICENSE | 21 +++++++++++++++++++++ package.json | 1 + scripts/check.mjs | 14 ++++++++++++-- tests/package-manifest.test.mjs | 31 +++++++++++++++++++++++++------ 4 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ca0af4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Justin Ling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json index d194038..7a8ddc3 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "diffsplain", "version": "0.13.0", "description": "Review Git diffs one file at a time with coding agent notes beside each patch.", + "license": "MIT", "keywords": [ "codex", "claude", diff --git a/scripts/check.mjs b/scripts/check.mjs index 246fd60..1eb9769 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -68,6 +68,7 @@ const releaseTarball = : resolve(root, process.argv[releaseTarballIndex + 1]); const requiredPackageFiles = [ 'README.md', + 'LICENSE', 'package.json', 'dist/index.html', 'scripts/access-token.mjs', @@ -98,7 +99,7 @@ const requiredPackageFiles = [ const allowedPackageFile = /^(README(?:\.md)?|LICENSE(?:\.md)?|NOTICE(?:\.md)?|package\.json|dist\/.+|scripts\/(?:access-token|agent-config|agent-exclusions|agent-note-output|agent-review|agent-usage|build-diff-data|cache|cli-args|coding-agents|dev|doctor|generate-summaries|local-target|mock-agent|present|presenter-runtime|review-chat(?:-context|-controller|-provider)?|serve-built|summary-path|support-record)\.mjs)$/; const privatePackageFile = /(^|\/)(?:\.env|\.npmrc|\.git|\.github|\.agents|\.codex)(?:\/|$)|\.(?:pem|key)$/i; -export function validatePackageManifest(pack) { +export function validatePackageManifest(pack, packageJson = {}) { const files = pack.files ?? []; const paths = new Set(files.map((file) => file.path)); const missing = requiredPackageFiles.filter((path) => !paths.has(path)); @@ -118,6 +119,15 @@ export function validatePackageManifest(pack) { }, { present: oversizedPackage, text: 'package exceeds 12 MB' }, { present: oversizedFile, text: 'file exceeds 1 MB' }, + { + present: packageJson.license === undefined, + text: 'missing package license', + }, + { + present: + packageJson.license !== undefined && packageJson.license !== 'MIT', + text: `license ${packageJson.license} does not match MIT`, + }, ] .filter((problem) => problem.present) .map((problem) => problem.text); @@ -220,7 +230,6 @@ async function smokeTestPackage() { const tarball = isAbsolute(pack.filename) ? pack.filename : join(packageRoot, pack.filename); - validatePackageManifest(pack); await mkdir(consumerRoot); await writeFile( join(consumerRoot, 'package.json'), @@ -234,6 +243,7 @@ async function smokeTestPackage() { const packageJson = JSON.parse( await readFile(join(consumerRoot, 'node_modules/diffsplain/package.json')), ); + validatePackageManifest(pack, packageJson); const executable = resolve( consumerRoot, 'node_modules/diffsplain', diff --git a/tests/package-manifest.test.mjs b/tests/package-manifest.test.mjs index f4cf093..b3ec3d9 100644 --- a/tests/package-manifest.test.mjs +++ b/tests/package-manifest.test.mjs @@ -4,6 +4,7 @@ import { validatePackageManifest } from '../scripts/check.mjs'; const requiredFiles = [ 'README.md', + 'LICENSE', 'package.json', 'dist/index.html', 'scripts/access-token.mjs', @@ -37,28 +38,46 @@ function manifest(files = requiredFiles.map((path) => ({ path, size: 1 }))) { } test('accepts the required package manifest', () => { - assert.doesNotThrow(() => validatePackageManifest(manifest())); + assert.doesNotThrow(() => validatePackageManifest(manifest(), { license: 'MIT' })); }); test('rejects missing, private, unexpected, and oversized package files', () => { assert.throws( - () => validatePackageManifest(manifest(requiredFiles.slice(1).map((path) => ({ path, size: 1 })))), + () => validatePackageManifest(manifest(requiredFiles.slice(1).map((path) => ({ path, size: 1 }))), { license: 'MIT' }), /missing README\.md/, ); assert.throws( - () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: '.env', size: 1 }])), + () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: '.env', size: 1 }]), { license: 'MIT' }), /private .env/, ); assert.throws( - () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'notes.txt', size: 1 }])), + () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'notes.txt', size: 1 }]), { license: 'MIT' }), /unexpected notes\.txt/, ); assert.throws( - () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'dist/large.js', size: 1_000_001 }])), + () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'dist/large.js', size: 1_000_001 }]), { license: 'MIT' }), /file exceeds 1 MB/, ); assert.throws( - () => validatePackageManifest({ ...manifest(), unpackedSize: 12_000_001 }), + () => validatePackageManifest({ ...manifest(), unpackedSize: 12_000_001 }, { license: 'MIT' }), /package exceeds 12 MB/, ); }); + +test('rejects missing license text or metadata that is not MIT', () => { + assert.throws( + () => validatePackageManifest( + manifest(requiredFiles.filter((path) => path !== 'LICENSE').map((path) => ({ path, size: 1 }))), + { license: 'MIT' }, + ), + /missing LICENSE/, + ); + assert.throws( + () => validatePackageManifest(manifest(), {}), + /missing package license/, + ); + assert.throws( + () => validatePackageManifest(manifest(), { license: 'Apache-2.0' }), + /license Apache-2\.0 does not match MIT/, + ); +}); From 3342588f2aeb9030ce7c68203d96f7de90311e4c Mon Sep 17 00:00:00 2001 From: Justin Ling <2521993+itsjling@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:37:43 +0000 Subject: [PATCH 2/4] Validate packed MIT license text --- scripts/check.mjs | 22 ++++++++++++++++-- tests/package-manifest.test.mjs | 40 +++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/scripts/check.mjs b/scripts/check.mjs index 1eb9769..4163ae4 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -1,4 +1,5 @@ import { execFile, spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; import { chmod, copyFile, @@ -98,8 +99,14 @@ const requiredPackageFiles = [ ]; const allowedPackageFile = /^(README(?:\.md)?|LICENSE(?:\.md)?|NOTICE(?:\.md)?|package\.json|dist\/.+|scripts\/(?:access-token|agent-config|agent-exclusions|agent-note-output|agent-review|agent-usage|build-diff-data|cache|cli-args|coding-agents|dev|doctor|generate-summaries|local-target|mock-agent|present|presenter-runtime|review-chat(?:-context|-controller|-provider)?|serve-built|summary-path|support-record)\.mjs)$/; const privatePackageFile = /(^|\/)(?:\.env|\.npmrc|\.git|\.github|\.agents|\.codex)(?:\/|$)|\.(?:pem|key)$/i; +const mitLicenseSha256 = + '6a716032ab0de9dbd312e06686f5b89996de8908c65425fa48bd5dbb081444a7'; -export function validatePackageManifest(pack, packageJson = {}) { +export function validatePackageManifest( + pack, + packageJson = {}, + licenseText = '', +) { const files = pack.files ?? []; const paths = new Set(files.map((file) => file.path)); const missing = requiredPackageFiles.filter((path) => !paths.has(path)); @@ -128,6 +135,14 @@ export function validatePackageManifest(pack, packageJson = {}) { packageJson.license !== undefined && packageJson.license !== 'MIT', text: `license ${packageJson.license} does not match MIT`, }, + { present: licenseText.length === 0, text: 'missing license text' }, + { + present: + licenseText.length > 0 && + createHash('sha256').update(licenseText).digest('hex') !== + mitLicenseSha256, + text: 'license text does not match MIT', + }, ] .filter((problem) => problem.present) .map((problem) => problem.text); @@ -243,7 +258,10 @@ async function smokeTestPackage() { const packageJson = JSON.parse( await readFile(join(consumerRoot, 'node_modules/diffsplain/package.json')), ); - validatePackageManifest(pack, packageJson); + const licenseText = pack.files?.some((file) => file.path === 'LICENSE') + ? await readFile(join(consumerRoot, 'node_modules/diffsplain/LICENSE'), 'utf8') + : ''; + validatePackageManifest(pack, packageJson, licenseText); const executable = resolve( consumerRoot, 'node_modules/diffsplain', diff --git a/tests/package-manifest.test.mjs b/tests/package-manifest.test.mjs index b3ec3d9..60c6c6e 100644 --- a/tests/package-manifest.test.mjs +++ b/tests/package-manifest.test.mjs @@ -1,7 +1,10 @@ import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; import test from 'node:test'; import { validatePackageManifest } from '../scripts/check.mjs'; +const mitLicense = await readFile(new URL('../LICENSE', import.meta.url), 'utf8'); + const requiredFiles = [ 'README.md', 'LICENSE', @@ -37,47 +40,60 @@ function manifest(files = requiredFiles.map((path) => ({ path, size: 1 }))) { return { files, unpackedSize: 1 }; } +function validate( + pack = manifest(), + packageJson = { license: 'MIT' }, + licenseText = mitLicense, +) { + return validatePackageManifest(pack, packageJson, licenseText); +} + test('accepts the required package manifest', () => { - assert.doesNotThrow(() => validatePackageManifest(manifest(), { license: 'MIT' })); + assert.doesNotThrow(() => validate()); }); test('rejects missing, private, unexpected, and oversized package files', () => { assert.throws( - () => validatePackageManifest(manifest(requiredFiles.slice(1).map((path) => ({ path, size: 1 }))), { license: 'MIT' }), + () => validate(manifest(requiredFiles.slice(1).map((path) => ({ path, size: 1 })))), /missing README\.md/, ); assert.throws( - () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: '.env', size: 1 }]), { license: 'MIT' }), + () => validate(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: '.env', size: 1 }])), /private .env/, ); assert.throws( - () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'notes.txt', size: 1 }]), { license: 'MIT' }), + () => validate(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'notes.txt', size: 1 }])), /unexpected notes\.txt/, ); assert.throws( - () => validatePackageManifest(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'dist/large.js', size: 1_000_001 }]), { license: 'MIT' }), + () => validate(manifest([...requiredFiles.map((path) => ({ path, size: 1 })), { path: 'dist/large.js', size: 1_000_001 }])), /file exceeds 1 MB/, ); assert.throws( - () => validatePackageManifest({ ...manifest(), unpackedSize: 12_000_001 }, { license: 'MIT' }), + () => validate({ ...manifest(), unpackedSize: 12_000_001 }), /package exceeds 12 MB/, ); }); test('rejects missing license text or metadata that is not MIT', () => { assert.throws( - () => validatePackageManifest( - manifest(requiredFiles.filter((path) => path !== 'LICENSE').map((path) => ({ path, size: 1 }))), - { license: 'MIT' }, - ), + () => validate(manifest(requiredFiles.filter((path) => path !== 'LICENSE').map((path) => ({ path, size: 1 })))), /missing LICENSE/, ); assert.throws( - () => validatePackageManifest(manifest(), {}), + () => validate(manifest(), {}), /missing package license/, ); assert.throws( - () => validatePackageManifest(manifest(), { license: 'Apache-2.0' }), + () => validate(manifest(), { license: 'Apache-2.0' }), /license Apache-2\.0 does not match MIT/, ); + assert.throws( + () => validate(manifest(), { license: 'MIT' }, ''), + /missing license text/, + ); + assert.throws( + () => validate(manifest(), { license: 'MIT' }, `${mitLicense}extra terms\n`), + /license text does not match MIT/, + ); }); From 76850d68dc3604dbdc39b0c64bd172ff549ac2c1 Mon Sep 17 00:00:00 2001 From: Justin Ling <2521993+itsjling@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:52:52 +0000 Subject: [PATCH 3/4] Keep license checks below audit limits --- scripts/check.mjs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/scripts/check.mjs b/scripts/check.mjs index 4163ae4..bb69ae0 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -102,11 +102,23 @@ const privatePackageFile = /(^|\/)(?:\.env|\.npmrc|\.git|\.github|\.agents|\.cod const mitLicenseSha256 = '6a716032ab0de9dbd312e06686f5b89996de8908c65425fa48bd5dbb081444a7'; +function validateMitLicenseText(licenseText) { + if (licenseText.length === 0) { + throw new Error('Package manifest check failed: missing license text'); + } + if ( + createHash('sha256').update(licenseText).digest('hex') !== mitLicenseSha256 + ) { + throw new Error('Package manifest check failed: license text does not match MIT'); + } +} + export function validatePackageManifest( pack, packageJson = {}, licenseText = '', ) { + validateMitLicenseText(licenseText); const files = pack.files ?? []; const paths = new Set(files.map((file) => file.path)); const missing = requiredPackageFiles.filter((path) => !paths.has(path)); @@ -135,14 +147,6 @@ export function validatePackageManifest( packageJson.license !== undefined && packageJson.license !== 'MIT', text: `license ${packageJson.license} does not match MIT`, }, - { present: licenseText.length === 0, text: 'missing license text' }, - { - present: - licenseText.length > 0 && - createHash('sha256').update(licenseText).digest('hex') !== - mitLicenseSha256, - text: 'license text does not match MIT', - }, ] .filter((problem) => problem.present) .map((problem) => problem.text); @@ -225,6 +229,14 @@ function verifySmokeResults({ packageJson, version, help, doctor, runtime }) { } } +function readPackedLicense(pack, consumerRoot) { + if (!pack.files?.some((file) => file.path === 'LICENSE')) return ''; + return readFile( + join(consumerRoot, 'node_modules/diffsplain/LICENSE'), + 'utf8', + ); +} + async function smokeTestPackage() { const packageRoot = await mkdtemp(join(tmpdir(), 'diffsplain-package-')); const consumerRoot = join(packageRoot, 'consumer'); @@ -258,9 +270,7 @@ async function smokeTestPackage() { const packageJson = JSON.parse( await readFile(join(consumerRoot, 'node_modules/diffsplain/package.json')), ); - const licenseText = pack.files?.some((file) => file.path === 'LICENSE') - ? await readFile(join(consumerRoot, 'node_modules/diffsplain/LICENSE'), 'utf8') - : ''; + const licenseText = await readPackedLicense(pack, consumerRoot); validatePackageManifest(pack, packageJson, licenseText); const executable = resolve( consumerRoot, From 877d8634881db5b3b193588bbf5c5e94c67c0cfb Mon Sep 17 00:00:00 2001 From: Justin Ling <2521993+itsjling@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:08:43 +0000 Subject: [PATCH 4/4] Accept canonical MIT text with CRLF --- scripts/check.mjs | 4 +++- tests/package-manifest.test.mjs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check.mjs b/scripts/check.mjs index bb69ae0..2c89250 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -107,7 +107,9 @@ function validateMitLicenseText(licenseText) { throw new Error('Package manifest check failed: missing license text'); } if ( - createHash('sha256').update(licenseText).digest('hex') !== mitLicenseSha256 + createHash('sha256') + .update(licenseText.replaceAll('\r\n', '\n')) + .digest('hex') !== mitLicenseSha256 ) { throw new Error('Package manifest check failed: license text does not match MIT'); } diff --git a/tests/package-manifest.test.mjs b/tests/package-manifest.test.mjs index 60c6c6e..03c2163 100644 --- a/tests/package-manifest.test.mjs +++ b/tests/package-manifest.test.mjs @@ -50,6 +50,9 @@ function validate( test('accepts the required package manifest', () => { assert.doesNotThrow(() => validate()); + assert.doesNotThrow(() => + validate(manifest(), { license: 'MIT' }, mitLicense.replaceAll('\n', '\r\n')), + ); }); test('rejects missing, private, unexpected, and oversized package files', () => {