Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
258 lines
6.7 KiB
TypeScript
258 lines
6.7 KiB
TypeScript
#!/usr/bin/env npx tsx
|
||
|
||
/**
|
||
* Test License Key Generator
|
||
*
|
||
* This script generates various license keys for manual testing of the LicenseManager.
|
||
* Uses the existing test utilities from LicenseManager.test.ts
|
||
*/
|
||
|
||
import crypto from 'crypto'
|
||
import { FLAGS } from '../../packages/editor/src/lib/license/LicenseManager'
|
||
import { str2ab } from '../../packages/editor/src/lib/utils/licensing'
|
||
// Utility function copied from LicenseManager.test.ts to avoid import issues
|
||
function ab2str(buf: ArrayBuffer) {
|
||
return String.fromCharCode.apply(null, Array.from(new Uint8Array(buf)))
|
||
}
|
||
|
||
// Utility functions copied from LicenseManager.test.ts (not exported)
|
||
async function generateLicenseKey(
|
||
message: string,
|
||
keyPair: { publicKey: string; privateKey: string }
|
||
) {
|
||
const enc = new TextEncoder()
|
||
const encodedMsg = enc.encode(message)
|
||
const privateKey = await importPrivateKey(keyPair.privateKey)
|
||
|
||
const signedLicenseKeyBuffer = await crypto.subtle.sign(
|
||
{
|
||
name: 'ECDSA',
|
||
hash: { name: 'SHA-256' },
|
||
},
|
||
privateKey,
|
||
encodedMsg
|
||
)
|
||
|
||
const signature = btoa(ab2str(signedLicenseKeyBuffer))
|
||
const prefix = 'tldraw-'
|
||
const licenseKey = `${prefix}/${btoa(message)}.${signature}`
|
||
|
||
return licenseKey
|
||
}
|
||
|
||
function importPrivateKey(pemContents: string) {
|
||
const binaryDerString = atob(pemContents)
|
||
const binaryDer = str2ab(binaryDerString)
|
||
|
||
return crypto.subtle.importKey(
|
||
'pkcs8',
|
||
new Uint8Array(binaryDer),
|
||
{
|
||
name: 'ECDSA',
|
||
namedCurve: 'P-256',
|
||
},
|
||
true,
|
||
['sign']
|
||
)
|
||
}
|
||
|
||
async function exportCryptoKey(key: CryptoKey, isPublic = false) {
|
||
const exported = await crypto.subtle.exportKey(isPublic ? 'spki' : 'pkcs8', key)
|
||
return btoa(ab2str(exported))
|
||
}
|
||
|
||
// Generate a fresh key pair for each run
|
||
async function generateKeyPair() {
|
||
const keyPair = await crypto.subtle.generateKey(
|
||
{
|
||
name: 'ECDSA',
|
||
namedCurve: 'P-256',
|
||
},
|
||
true,
|
||
['sign', 'verify']
|
||
)
|
||
const publicKey = await exportCryptoKey(keyPair.publicKey, true /* isPublic */)
|
||
const privateKey = await exportCryptoKey(keyPair.privateKey)
|
||
|
||
return { publicKey, privateKey }
|
||
}
|
||
|
||
function createLicenseInfo(id: string, hosts: string[], flags: number, expiryDate: string) {
|
||
return JSON.stringify([id, hosts, flags, expiryDate])
|
||
}
|
||
|
||
function getDateOffset(days: number): string {
|
||
const date = new Date()
|
||
date.setDate(date.getDate() + days)
|
||
return date.toISOString().slice(0, 10)
|
||
}
|
||
|
||
async function main() {
|
||
console.log('🔑 Generating test license keys for LicenseManager testing...\n')
|
||
|
||
console.log('Generating fresh key pair for testing...')
|
||
const keyPair = await generateKeyPair()
|
||
console.log('Public Key:', keyPair.publicKey)
|
||
console.log('Private Key:', keyPair.privateKey)
|
||
console.log('')
|
||
|
||
const testCases = [
|
||
// === VALID LICENSES ===
|
||
{
|
||
name: 'Valid Annual License',
|
||
info: createLicenseInfo(
|
||
'test-annual-valid',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE,
|
||
getDateOffset(60) // 2 months future
|
||
),
|
||
},
|
||
{
|
||
name: 'Valid Perpetual License',
|
||
info: createLicenseInfo(
|
||
'test-perpetual-valid',
|
||
['localhost'],
|
||
FLAGS.PERPETUAL_LICENSE,
|
||
getDateOffset(365) // 1 year future
|
||
),
|
||
},
|
||
{
|
||
name: 'Valid Evaluation License',
|
||
info: createLicenseInfo(
|
||
'test-eval-valid',
|
||
['localhost'],
|
||
FLAGS.EVALUATION_LICENSE,
|
||
getDateOffset(14) // 2 weeks future
|
||
),
|
||
},
|
||
{
|
||
name: 'Valid Internal License',
|
||
info: createLicenseInfo(
|
||
'test-internal-valid',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE | FLAGS.INTERNAL_LICENSE,
|
||
getDateOffset(60) // 2 months future
|
||
),
|
||
},
|
||
{
|
||
name: 'License with Watermark Flag',
|
||
info: createLicenseInfo(
|
||
'test-watermark-only',
|
||
['localhost'],
|
||
FLAGS.WITH_WATERMARK,
|
||
getDateOffset(60) // 2 months future
|
||
),
|
||
},
|
||
|
||
// === FEATURE FLAGS ===
|
||
{
|
||
name: 'License with Commenting Feature',
|
||
info: createLicenseInfo(
|
||
'test-feat-commenting',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE | FLAGS.FEAT_COMMENTING,
|
||
getDateOffset(60) // 2 months future
|
||
),
|
||
},
|
||
{
|
||
name: 'License with Collaboration Feature (grants commenting)',
|
||
info: createLicenseInfo(
|
||
'test-feat-collaboration',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE | FLAGS.FEAT_COLLABORATION,
|
||
getDateOffset(60) // 2 months future
|
||
),
|
||
},
|
||
|
||
// === GRACE PERIOD TESTING ===
|
||
{
|
||
name: 'Annual License (expired 15 days - within grace period)',
|
||
info: createLicenseInfo(
|
||
'test-grace-15days',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE,
|
||
getDateOffset(-15) // 15 days past
|
||
),
|
||
},
|
||
{
|
||
name: 'Annual License (expired 45 days - beyond grace period)',
|
||
info: createLicenseInfo(
|
||
'test-annual-expired-45days',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE,
|
||
getDateOffset(-45) // 45 days past
|
||
),
|
||
},
|
||
|
||
// === EXPIRED INTERNAL LICENSES ===
|
||
{
|
||
name: 'Internal License (expired 15 days - within grace period)',
|
||
info: createLicenseInfo(
|
||
'test-internal-expired-15days',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE | FLAGS.INTERNAL_LICENSE,
|
||
getDateOffset(-15) // 15 days past
|
||
),
|
||
},
|
||
{
|
||
name: 'Internal License (expired 45 days - beyond grace period)',
|
||
info: createLicenseInfo(
|
||
'test-internal-expired-45days',
|
||
['localhost'],
|
||
FLAGS.ANNUAL_LICENSE | FLAGS.INTERNAL_LICENSE,
|
||
getDateOffset(-45) // 45 days past
|
||
),
|
||
},
|
||
|
||
// === EXPIRED EVALUATION LICENSES ===
|
||
{
|
||
name: 'Evaluation License (expired 1 day)',
|
||
info: createLicenseInfo(
|
||
'test-eval-expired-1day',
|
||
['localhost'],
|
||
FLAGS.EVALUATION_LICENSE,
|
||
getDateOffset(-1) // 1 day past
|
||
),
|
||
},
|
||
{
|
||
name: 'Evaluation License (expired 15 days)',
|
||
info: createLicenseInfo(
|
||
'test-eval-expired-15days',
|
||
['localhost'],
|
||
FLAGS.EVALUATION_LICENSE,
|
||
getDateOffset(-15) // 15 days past
|
||
),
|
||
},
|
||
]
|
||
|
||
console.log('🎫 Generated Test License Keys:\n')
|
||
|
||
for (const testCase of testCases) {
|
||
try {
|
||
const licenseKey = await generateLicenseKey(testCase.info, keyPair)
|
||
console.log(`## ${testCase.name}`)
|
||
console.log(`License Info: ${testCase.info}`)
|
||
console.log(`License Key: ${licenseKey}`)
|
||
console.log('')
|
||
} catch (error) {
|
||
console.error(`❌ Failed to generate license for ${testCase.name}:`, error)
|
||
}
|
||
}
|
||
|
||
console.log('📝 Usage Instructions:')
|
||
console.log('')
|
||
console.log('🚨 DO NOT COMMIT THESE KEYS TO THE REPOSITORY! 🚨')
|
||
console.log('These are temporary test keys - generate fresh ones for each session')
|
||
console.log('')
|
||
console.log('Option 1 - Update LicenseManager (development):')
|
||
console.log(` Replace publicKey in LicenseManager.ts with: "${keyPair.publicKey}"`)
|
||
console.log(' Then use: <Tldraw licenseKey="license-key-from-above" />')
|
||
console.log('')
|
||
console.log('Option 2 - Pass testPublicKey (automated testing):')
|
||
console.log(` new LicenseManager("license-key", "${keyPair.publicKey}", "production")`)
|
||
console.log('')
|
||
console.log('ℹ️ For production licenses: sales@tldraw.com')
|
||
}
|
||
|
||
main().catch(console.error)
|