Skip to content

Commit db29ea5

Browse files
feat(license): replace sponsored builds with supporter license keys (#821)
* feat(license): replace sponsored builds with offline supporter license keys * refactor(license): rewrite issue scripts in TypeScript for bun * Revert "refactor(license): rewrite issue scripts in TypeScript for bun" This reverts commit cd8f8f1. * fix(i18n): use i18next interpolation syntax in supporter keys * feat(license): make email the primary license identifier
1 parent e642d1b commit db29ea5

25 files changed

Lines changed: 614 additions & 147 deletions

File tree

.github/workflows/build-sponsored.yml

Lines changed: 0 additions & 85 deletions
This file was deleted.

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ build/renderer
44
build/shared
55
build/src
66
build/package.json
7-
scripts/build-sponsored.sh
87

98
node_modules
109
.DS_Store

electron-builder.sponsored.json

Lines changed: 0 additions & 47 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,11 @@
2222
"build:mac:arm64": "vite build && npm run build:main && electron-builder --mac --arm64",
2323
"build:win": "vite build && npm run build:main && electron-builder --win --x64",
2424
"build:linux": "vite build && npm run build:main && electron-builder --linux --x64",
25-
"build:sponsored:mac": "vite build && npm run build:main && npm run build:sponsored:mac:x64 && npm run build:sponsored:mac:arm64",
26-
"build:sponsored:mac:x64": "vite build && npm run build:main && electron-builder --config electron-builder.sponsored.json --mac --x64",
27-
"build:sponsored:mac:arm64": "vite build && npm run build:main && electron-builder --config electron-builder.sponsored.json --mac --arm64",
28-
"build:sponsored:win": "vite build && npm run build:main && electron-builder --config electron-builder.sponsored.json --win --x64",
29-
"build:sponsored:linux": "vite build && npm run build:main && electron-builder --config electron-builder.sponsored.json --linux --x64",
3025
"build:all": "vite build && npm run build:main && npm run build:mac && npm run build:win && npm run build:linux",
3126
"build:main": "npm run i18n:copy && tsc -p tsconfig.main.json",
3227
"api:generate": "node scripts/api-generate.js",
3328
"i18n:copy": "node scripts/i18n/copy-locales.js",
29+
"license:issue": "node scripts/license/issue.js",
3430
"i18n:check": "node scripts/i18n/check-locale-parity.mjs",
3531
"bench:load-test": "node scripts/bench-load-test.js",
3632
"bench:seed": "node scripts/bench-seed.js",

scripts/license/issue.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Выпуск supporter-лицензии, подписанной приватным ключом Ed25519.
3+
*
4+
* Запуск: pnpm license:issue --email john@example.com [--name "John Doe"]
5+
*/
6+
const { Buffer } = require('node:buffer')
7+
const { createPrivateKey, sign } = require('node:crypto')
8+
const fs = require('node:fs')
9+
const os = require('node:os')
10+
const path = require('node:path')
11+
const process = require('node:process')
12+
13+
const privateKeyPath
14+
= process.env.MASSCODE_LICENSE_PRIVATE_KEY
15+
|| path.join(os.homedir(), '.masscode', 'license-private.pem')
16+
17+
function getArg(name) {
18+
const index = process.argv.indexOf(`--${name}`)
19+
if (index === -1 || index === process.argv.length - 1) {
20+
return undefined
21+
}
22+
return process.argv[index + 1]
23+
}
24+
25+
const name = getArg('name')
26+
const email = getArg('email')
27+
28+
if (!email) {
29+
console.error(
30+
'Usage: pnpm license:issue --email john@example.com [--name "John Doe"]',
31+
)
32+
process.exit(1)
33+
}
34+
35+
if (!fs.existsSync(privateKeyPath)) {
36+
console.error(`Private key not found: ${privateKeyPath}`)
37+
console.error('Run "node scripts/license/keygen.js" first.')
38+
process.exit(1)
39+
}
40+
41+
const privateKey = createPrivateKey(fs.readFileSync(privateKeyPath))
42+
43+
const payload = {
44+
email,
45+
...(name ? { name } : {}),
46+
issuedAt: new Date().toISOString().slice(0, 10),
47+
}
48+
49+
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString(
50+
'base64url',
51+
)
52+
const signature = sign(null, Buffer.from(payloadBase64), privateKey).toString(
53+
'base64url',
54+
)
55+
56+
console.log(`${payloadBase64}.${signature}`)

scripts/license/keygen.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Одноразовая генерация пары ключей Ed25519 для supporter-лицензий.
3+
*
4+
* Приватный ключ сохраняется в ~/.masscode/license-private.pem и никогда
5+
* не должен попадать в репозиторий. Публичный ключ выводится в stdout —
6+
* его нужно вшить в src/main/license/index.ts (LICENSE_PUBLIC_KEY).
7+
*
8+
* Запуск: node scripts/license/keygen.js
9+
*/
10+
const { generateKeyPairSync } = require('node:crypto')
11+
const fs = require('node:fs')
12+
const os = require('node:os')
13+
const path = require('node:path')
14+
const process = require('node:process')
15+
16+
const privateKeyDir = path.join(os.homedir(), '.masscode')
17+
const privateKeyPath = path.join(privateKeyDir, 'license-private.pem')
18+
19+
if (fs.existsSync(privateKeyPath)) {
20+
console.error(`Private key already exists: ${privateKeyPath}`)
21+
console.error(
22+
'Remove it manually if you really want to generate a new pair.',
23+
)
24+
process.exit(1)
25+
}
26+
27+
const { publicKey, privateKey } = generateKeyPairSync('ed25519')
28+
29+
fs.mkdirSync(privateKeyDir, { recursive: true })
30+
fs.writeFileSync(
31+
privateKeyPath,
32+
privateKey.export({ type: 'pkcs8', format: 'pem' }),
33+
{ mode: 0o600 },
34+
)
35+
36+
const publicKeyBase64 = publicKey
37+
.export({ type: 'spki', format: 'der' })
38+
.toString('base64')
39+
40+
console.log(`Private key saved to: ${privateKeyPath}`)
41+
console.log('')
42+
console.log('Public key (embed into src/main/license/index.ts):')
43+
console.log(publicKeyBase64)

src/main/i18n/locales/en_US/messages.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"copied": "Copied to clipboard",
2121
"migrateToMarkdown": "Migrated to Markdown Vault. Folders: {{folders}}, snippets: {{snippets}}, tags: {{tags}}.",
2222
"vaultMoved": "Vault successfully moved.",
23-
"vaultLoaded": "Vault successfully loaded."
23+
"vaultLoaded": "Vault successfully loaded.",
24+
"licenseActivated": "License activated. Thank you for supporting massCode!"
2425
},
2526
"warning": {
2627
"noUndo": "You cannot undo this action.",
@@ -44,7 +45,8 @@
4445
"entryNameNoteConflict": "A note with this name already exists in this folder.",
4546
"entryNameSnippetConflict": "A snippet with this name already exists in this folder.",
4647
"entryNameRequestConflict": "A request with this name already exists in this folder.",
47-
"entryNameFolderConflict": "A folder with this name already exists at this level."
48+
"entryNameFolderConflict": "A folder with this name already exists at this level.",
49+
"licenseInvalid": "Invalid license key"
4850
},
4951
"description": {
5052
"storageVault": "Choose the vault directory. To sync between devices, select a folder in iCloud Drive, Google Drive or Dropbox.",

src/main/i18n/locales/en_US/preferences.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
},
126126
"decimalPlaces": {
127127
"label": "Decimal Places",
128-
"description": "Maximum decimal places in results (0\u201314)."
128+
"description": "Maximum decimal places in results (0–14)."
129129
},
130130
"dateFormat": {
131131
"label": "Date Format",
@@ -189,5 +189,25 @@
189189
"revoke": "Revoke token"
190190
}
191191
}
192+
},
193+
"supporter": {
194+
"label": "Supporter",
195+
"status": {
196+
"label": "Status",
197+
"active": "Supporter license is active.",
198+
"activeFor": "Supporter license is active for {{name}}.",
199+
"description": "Thank you for supporting massCode! Donation prompts are turned off."
200+
},
201+
"key": {
202+
"label": "License key",
203+
"placeholder": "Paste your supporter key",
204+
"activate": "Activate",
205+
"description": "Paste the key you received after donating. Activation works offline and removes donation prompts."
206+
},
207+
"request": {
208+
"label": "No key yet?",
209+
"action": "Request a key",
210+
"description": "If you have ever donated to massCode through any channel, send any proof of donation by email and you will receive a key."
211+
}
192212
}
193213
}

src/main/i18n/locales/ru_RU/messages.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"success": {
1616
"copied": "Скопировано в буфер обмена",
1717
"migrateToMarkdown": "Выполнена миграция в Markdown Vault. Папок: {{folders}}, сниппетов: {{snippets}}, тегов: {{tags}}.",
18-
"vaultLoaded": "Хранилище успешно загружено."
18+
"vaultLoaded": "Хранилище успешно загружено.",
19+
"licenseActivated": "Лицензия активирована. Спасибо за поддержку massCode!"
1920
},
2021
"warning": {
2122
"noUndo": "Это действие нельзя отменить.",
@@ -39,7 +40,8 @@
3940
"entryNameNoteConflict": "В этой папке уже есть заметка с таким именем.",
4041
"entryNameSnippetConflict": "В этой папке уже есть сниппет с таким именем.",
4142
"entryNameRequestConflict": "В этой папке уже есть запрос с таким именем.",
42-
"entryNameFolderConflict": "Папка с таким именем уже существует на этом уровне."
43+
"entryNameFolderConflict": "Папка с таким именем уже существует на этом уровне.",
44+
"licenseInvalid": "Недействительный лицензионный ключ"
4345
},
4446
"description": {
4547
"storageVault": "Выберите директорию для хранилища. Для синхронизации между устройствами выберите папку в iCloud Drive, Google Drive или Dropbox.",

src/main/i18n/locales/ru_RU/preferences.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,5 +156,25 @@
156156
"revoke": "Отозвать token"
157157
}
158158
}
159+
},
160+
"supporter": {
161+
"label": "Саппортер",
162+
"status": {
163+
"label": "Статус",
164+
"active": "Лицензия саппортера активна.",
165+
"activeFor": "Лицензия саппортера активна для {{name}}.",
166+
"description": "Спасибо за поддержку massCode! Напоминания о донатах отключены."
167+
},
168+
"key": {
169+
"label": "Лицензионный ключ",
170+
"placeholder": "Вставьте ключ саппортера",
171+
"activate": "Активировать",
172+
"description": "Вставьте ключ, полученный после доната. Активация работает офлайн и отключает напоминания о донатах."
173+
},
174+
"request": {
175+
"label": "Нет ключа?",
176+
"action": "Запросить ключ",
177+
"description": "Если вы когда-либо донатили massCode любым способом, отправьте любое подтверждение доната по почте и получите ключ."
178+
}
159179
}
160180
}

0 commit comments

Comments
 (0)