UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

1,005 lines (891 loc) • 35 kB
import crypto from 'crypto' import { vi } from 'vitest' import { publishDates } from '../../version' import { str2ab } from '../utils/licensing' import { FLAGS, getLicenseState, LicenseManager, PROPERTIES, ValidLicenseKeyResult, } from './LicenseManager' vi.mock('../../version', () => { return { version: '3.15.1', publishDates: { major: '2024-06-28T10:56:07.893Z', minor: '2024-07-02T16:49:50.397Z', patch: '2030-07-02T16:49:50.397Z', }, } }) const now = new Date() const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 5).toISOString() const STANDARD_LICENSE_INFO = JSON.stringify([ 'id', ['www.example.com'], FLAGS.ANNUAL_LICENSE, expiryDate, ]) describe('LicenseManager', () => { let keyPair: { publicKey: string; privateKey: string } let licenseManager: LicenseManager beforeAll(() => { process.env.NODE_ENV = 'production' return new Promise((resolve) => { generateKeyPair().then((kp) => { keyPair = kp licenseManager = new LicenseManager('', keyPair.publicKey) resolve(void 0) }) }) process.env.NODE_ENV = 'test' }) beforeEach(() => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://www.example.com') }) describe('Basic license validation', () => { it('Fails if no key provided', async () => { const result = await licenseManager.getLicenseFromKey('') expect(result).toMatchObject({ isLicenseParseable: false, reason: 'no-key-provided' }) }) it('Signals that it is development mode when localhost', async () => { const schemes = ['http', 'https'] for (const scheme of schemes) { // @ts-ignore delete window.location // @ts-ignore window.location = new URL(`${scheme}://localhost:3000`) const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey) const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey) expect(result).toMatchObject({ isLicenseParseable: true, isDomainValid: false, isDevelopment: true, }) } }) it('Signals that it is development mode when NODE_ENV is not production', async () => { process.env.NODE_ENV = 'development' // @ts-ignore delete window.location // @ts-ignore window.location = new URL(`https://www.example.com`) const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey) const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey) expect(result).toMatchObject({ isLicenseParseable: true, isDomainValid: true, isDevelopment: true, }) const licenseState = testEnvLicenseManager.state.get() expect(licenseState).toBe('unlicensed') process.env.NODE_ENV = 'test' }) it('Signals that it is development mode when NODE_ENV is "test"', async () => { process.env.NODE_ENV = 'test' // @ts-ignore delete window.location // @ts-ignore window.location = new URL(`https://www.example.com`) const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey) const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey) expect(result).toMatchObject({ isLicenseParseable: true, isDomainValid: true, isDevelopment: true, }) const licenseState = testEnvLicenseManager.state.get() expect(licenseState).toBe('unlicensed') process.env.NODE_ENV = 'test' }) it('Signals that it is not development mode when NODE_ENV is production', async () => { process.env.NODE_ENV = 'production' // @ts-ignore delete window.location // @ts-ignore window.location = new URL(`https://www.example.com`) const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey) const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey) expect(result).toMatchObject({ isLicenseParseable: true, isDomainValid: true, isDevelopment: false, }) const licenseState = testEnvLicenseManager.state.get() expect(licenseState).toBe('unlicensed-production') process.env.NODE_ENV = 'test' }) it('Cleanses out valid keys that accidentally have zero-width characters or newlines', async () => { const cleanLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const dirtyLicenseKey = cleanLicenseKey + '\u200B\u200D\uFEFF\n\r' const result = await licenseManager.getLicenseFromKey(dirtyLicenseKey) expect(result.isLicenseParseable).toBe(true) }) it('Fails if garbage key provided', async () => { process.env.NODE_ENV = 'production' const badPublicKeyLicenseManager = new LicenseManager('', 'badpublickey') const invalidLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = await badPublicKeyLicenseManager.getLicenseFromKey(invalidLicenseKey) expect(result).toMatchObject({ isLicenseParseable: false, reason: 'invalid-license-key' }) process.env.NODE_ENV = 'test' }) it('Fails if non-JSON parseable message is provided', async () => { const invalidMessage = await generateLicenseKey('asdfsad', keyPair) const result = await licenseManager.getLicenseFromKey(invalidMessage) expect(result).toMatchObject({ isLicenseParseable: false, reason: 'invalid-license-key' }) }) it('Succeeds if valid key provided', async () => { const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = await licenseManager.getLicenseFromKey(licenseKey) expect(result).toMatchObject({ isLicenseParseable: true, license: { id: 'id', hosts: ['www.example.com'], flags: FLAGS.ANNUAL_LICENSE, expiryDate, }, isDomainValid: true, isAnnualLicense: true, isAnnualLicenseExpired: false, isPerpetualLicense: false, isPerpetualLicenseExpired: false, isInternalLicense: false, isEvaluationLicense: false, isEvaluationLicenseExpired: false, daysSinceExpiry: 0, } as ValidLicenseKeyResult) }) }) describe('Domain validation', () => { it('Fails with invalid host', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://www.foo.com') const expiredLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair) const result = (await licenseManager.getLicenseFromKey( expiredLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(false) }) it('Succeeds if hosts is equal to only "*"', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://www.foo.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['*'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if has an apex domain specified', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://www.example.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['example.com'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if has an www domain specified, but at the apex domain', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://example.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['www.example.com'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if has a subdomain wildcard', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://sub.example.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if has a sub-subdomain wildcard', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://pr-2408.sub.example.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if has a subdomain wildcard but on an apex domain', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://example.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Fails if has a subdomain wildcard isnt for the same base domain', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('https://sub.example.com') const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.foo.com'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(false) }) it('Succeeds if it is a vscode extension', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL( 'vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app' ) const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['tldraw-org.tldraw-vscode'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Fails if it is a vscode extension with the wrong id', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL( 'vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app' ) const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO) permissiveHostsInfo[PROPERTIES.HOSTS] = ['blah-org.blah-vscode'] const permissiveLicenseKey = await generateLicenseKey( JSON.stringify(permissiveHostsInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( permissiveLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(false) }) it('Succeeds if it is a native app', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('app-bundle://app/index.html') const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:'] const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair) const result = (await licenseManager.getLicenseFromKey( nativeLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if it is a native app with a wildcard', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('app-bundle://unique-id-123/index.html') const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE nativeLicenseInfo[PROPERTIES.HOSTS] = ['^app-bundle://unique-id-123.*'] const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair) const result = (await licenseManager.getLicenseFromKey( nativeLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Succeeds if it is a native app with a wildcard and search param', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('app-bundle://app/index.html?unique-id-123') const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE nativeLicenseInfo[PROPERTIES.HOSTS] = ['^app-bundle://app.*unique-id-123.*'] const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair) const result = (await licenseManager.getLicenseFromKey( nativeLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(true) }) it('Fails if it is a native app with the wrong protocol', async () => { // @ts-ignore delete window.location // @ts-ignore window.location = new URL('blah-blundle://app/index.html') const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:'] const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair) const result = (await licenseManager.getLicenseFromKey( nativeLicenseKey )) as ValidLicenseKeyResult expect(result.isDomainValid).toBe(false) }) }) describe('License types and flags', () => { it('Checks for internal license', async () => { const internalLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) internalLicenseInfo[PROPERTIES.FLAGS] = FLAGS.INTERNAL_LICENSE const internalLicenseKey = await generateLicenseKey( JSON.stringify(internalLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( internalLicenseKey )) as ValidLicenseKeyResult expect(result.isInternalLicense).toBe(true) }) it('Checks for native license', async () => { const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair) const result = (await licenseManager.getLicenseFromKey( nativeLicenseKey )) as ValidLicenseKeyResult expect(result.isNativeLicense).toBe(true) }) it('Checks for license with watermark', async () => { const withWatermarkLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) withWatermarkLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.WITH_WATERMARK const withWatermarkLicenseKey = await generateLicenseKey( JSON.stringify(withWatermarkLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( withWatermarkLicenseKey )) as ValidLicenseKeyResult expect(result.isLicensedWithWatermark).toBe(true) }) it('Checks for evaluation license', async () => { const evaluationLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) evaluationLicenseInfo[PROPERTIES.FLAGS] = FLAGS.EVALUATION_LICENSE const evaluationLicenseKey = await generateLicenseKey( JSON.stringify(evaluationLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( evaluationLicenseKey )) as ValidLicenseKeyResult expect(result.isEvaluationLicense).toBe(true) expect(result.isEvaluationLicenseExpired).toBe(false) }) it('Detects when evaluation license has expired', async () => { const expiredEvaluationLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) expiredEvaluationLicenseInfo[PROPERTIES.FLAGS] = FLAGS.EVALUATION_LICENSE const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1) // 1 day ago expiredEvaluationLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString() const expiredEvaluationLicenseKey = await generateLicenseKey( JSON.stringify(expiredEvaluationLicenseInfo), keyPair ) // The getLicenseFromKey should return the expired state const result = (await licenseManager.getLicenseFromKey( expiredEvaluationLicenseKey )) as ValidLicenseKeyResult expect(result.isEvaluationLicense).toBe(true) expect(result.isEvaluationLicenseExpired).toBe(true) }) }) describe('License expiry and grace period', () => { it('Fails if the license key has expired beyond grace period', async () => { const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 40) // 40 days ago (beyond 30-day grace period) expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiryDate const expiredLicenseKey = await generateLicenseKey( JSON.stringify(expiredLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( expiredLicenseKey )) as ValidLicenseKeyResult expect(result.isAnnualLicenseExpired).toBe(true) }) it('Allows a grace period for expired licenses', async () => { const almostExpiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 5) // 5 days ago almostExpiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiryDate const almostExpiredLicenseKey = await generateLicenseKey( JSON.stringify(almostExpiredLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( almostExpiredLicenseKey )) as ValidLicenseKeyResult expect(result.isAnnualLicenseExpired).toBe(false) }) it('Handles grace period correctly - 20 days expired should still be within grace period', async () => { const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 20) // 20 days ago expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString() const expiredLicenseKey = await generateLicenseKey( JSON.stringify(expiredLicenseInfo), keyPair ) // Test the getLicenseFromKey method to verify grace period calculation const result = (await licenseManager.getLicenseFromKey( expiredLicenseKey )) as ValidLicenseKeyResult expect(result.isAnnualLicense).toBe(true) expect(result.isAnnualLicenseExpired).toBe(false) // Within 30-day grace period expect(result.daysSinceExpiry).toBe(20) }) it('Calculates days since expiry correctly', async () => { const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO) const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 15) // 15 days ago expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString() const expiredLicenseKey = await generateLicenseKey( JSON.stringify(expiredLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( expiredLicenseKey )) as ValidLicenseKeyResult expect(result.daysSinceExpiry).toBe(15) }) }) describe('Perpetual licenses', () => { // We mock the patch version to be in 2030 above. it('Succeeds for perpetual license with correct version (and patch does not matter)', async () => { const majorDate = new Date(publishDates.major) const expiryDate = new Date( majorDate.getFullYear(), majorDate.getMonth(), majorDate.getDate() + 100 ) const perpetualLicenseInfo = ['id', ['www.example.com'], FLAGS.PERPETUAL_LICENSE, expiryDate] const almostExpiredLicenseKey = await generateLicenseKey( JSON.stringify(perpetualLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( almostExpiredLicenseKey )) as ValidLicenseKeyResult expect(result.isPerpetualLicense).toBe(true) expect(result.isPerpetualLicenseExpired).toBe(false) }) it('Fails for perpetual license past the release version', async () => { const majorDate = new Date(publishDates.major) const expiryDate = new Date( majorDate.getFullYear(), majorDate.getMonth(), majorDate.getDate() - 100 ) const perpetualLicenseInfo = ['id', ['www.example.com'], FLAGS.PERPETUAL_LICENSE, expiryDate] const almostExpiredLicenseKey = await generateLicenseKey( JSON.stringify(perpetualLicenseInfo), keyPair ) const result = (await licenseManager.getLicenseFromKey( almostExpiredLicenseKey )) as ValidLicenseKeyResult expect(result.isPerpetualLicense).toBe(true) expect(result.isPerpetualLicenseExpired).toBe(true) }) }) }) 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 } /* Import a PEM encoded RSA private key, to use for RSA-PSS signing. Takes a string containing the PEM encoded key, and returns a Promise that will resolve to a CryptoKey representing the private key. */ function importPrivateKey(pemContents: string) { // base64 decode the string to get the binary data const binaryDerString = atob(pemContents) // convert from a binary string to an ArrayBuffer const binaryDer = str2ab(binaryDerString) return crypto.subtle.importKey( 'pkcs8', new Uint8Array(binaryDer), { name: 'ECDSA', namedCurve: 'P-256', }, true, ['sign'] ) } /* Generate a sign/verify key pair. */ 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 } } async function exportCryptoKey(key: CryptoKey, isPublic = false) { const exported = await crypto.subtle.exportKey(isPublic ? 'spki' : 'pkcs8', key) return btoa(ab2str(exported)) } /* Convert an ArrayBuffer into a string from https://developer.chrome.com/blog/how-to-convert-arraybuffer-to-and-from-string/ */ export function ab2str(buf: ArrayBuffer) { return String.fromCharCode.apply(null, new Uint8Array(buf) as unknown as number[]) } function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): ValidLicenseKeyResult { return { isAnnualLicense: true, isAnnualLicenseExpired: false, isInternalLicense: false, isNativeLicense: false, isDevelopment: false, isDomainValid: true, isPerpetualLicense: false, isPerpetualLicenseExpired: false, isLicenseParseable: true as const, isLicensedWithWatermark: false, isEvaluationLicense: false, isEvaluationLicenseExpired: false, daysSinceExpiry: 0, // WatermarkManager does not check these fields, it relies on the calculated values like isAnnualLicenseExpired license: { id: 'id', hosts: ['localhost'], flags: FLAGS.PERPETUAL_LICENSE, expiryDate: new Date().toISOString(), }, expiryDate: new Date(), ...overrides, } } describe('getLicenseState', () => { describe('Development mode', () => { it('returns "unlicensed" for unparseable license in development', () => { const licenseResult = getDefaultLicenseResult({ // @ts-ignore isLicenseParseable: false, }) expect(getLicenseState(licenseResult, () => {}, true)).toBe('unlicensed') }) it('returns "licensed" for invalid domain in development mode', () => { const licenseResult = getDefaultLicenseResult({ isDomainValid: false, isDevelopment: true, }) expect(getLicenseState(licenseResult, () => {}, true)).toBe('licensed') }) it('returns "licensed" for valid license in development mode', () => { const licenseResult = getDefaultLicenseResult({ isDevelopment: true, }) expect(getLicenseState(licenseResult, () => {}, true)).toBe('licensed') }) it('returns "unlicensed" for no license key in development (localhost)', () => { const licenseResult = getDefaultLicenseResult({ // @ts-ignore isLicenseParseable: false, reason: 'no-key-provided', }) expect(getLicenseState(licenseResult, () => {}, true)).toBe('unlicensed') }) }) describe('Production mode - unlicensed states', () => { it('returns "unlicensed-production" for unparseable license in production (invalid-license-key)', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ // @ts-ignore isLicenseParseable: false, reason: 'invalid-license-key', }) const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false) expect(result).toBe('unlicensed-production') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Invalid license key. tldraw requires a valid license for production use.', 'Please reach out to sales@tldraw.com to purchase a license.', ]) }) it('returns "unlicensed-production" for no license key in production', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ // @ts-ignore isLicenseParseable: false, reason: 'no-key-provided', }) const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false) expect(result).toBe('unlicensed-production') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'No tldraw license key provided!', 'A license is required for production deployments.', 'Please reach out to sales@tldraw.com to purchase a license.', ]) }) it('returns "unlicensed-production" for invalid license key in production', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ // @ts-ignore isLicenseParseable: false, reason: 'invalid-license-key', }) const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false) expect(result).toBe('unlicensed-production') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Invalid license key. tldraw requires a valid license for production use.', 'Please reach out to sales@tldraw.com to purchase a license.', ]) }) it('returns "unlicensed-production" for invalid domain in production', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ isDomainValid: false, isDevelopment: false, }) const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false) expect(result).toBe('unlicensed-production') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'License key is not valid for this domain.', 'A license is required for production deployments.', 'Please reach out to sales@tldraw.com to purchase a license.', ]) }) it('returns "unlicensed-production" for expired internal license with invalid domain', () => { const messages: string[][] = [] const expiryDate = new Date(2023, 1, 1) const licenseResult = getDefaultLicenseResult({ isAnnualLicense: true, isAnnualLicenseExpired: true, isInternalLicense: true, isDomainValid: false, expiryDate, }) const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false) expect(result).toBe('unlicensed-production') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'License key is not valid for this domain.', 'A license is required for production deployments.', 'Please reach out to sales@tldraw.com to purchase a license.', ]) }) }) describe('Valid licenses', () => { it('returns "licensed" for valid annual license', () => { const licenseResult = getDefaultLicenseResult({ isAnnualLicense: true, isAnnualLicenseExpired: false, }) expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed') }) it('returns "licensed" for valid perpetual license', () => { const licenseResult = getDefaultLicenseResult({ isPerpetualLicense: true, isPerpetualLicenseExpired: false, }) expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed') }) it('returns "licensed-with-watermark" for watermarked license', () => { const licenseResult = getDefaultLicenseResult({ isLicensedWithWatermark: true, }) expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed-with-watermark') }) it('returns "licensed" for valid evaluation license', () => { const licenseResult = getDefaultLicenseResult({ isEvaluationLicense: true, isLicensedWithWatermark: false, // Evaluation license doesn't need WITH_WATERMARK flag isAnnualLicense: false, isPerpetualLicense: false, }) // Evaluation license should be licensed but tracked (no watermark shown) expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed') // Verify evaluation license properties expect(licenseResult.isEvaluationLicense).toBe(true) expect(licenseResult.isLicensedWithWatermark).toBe(false) // No explicit watermark flag needed expect(licenseResult.isAnnualLicense).toBe(false) expect(licenseResult.isPerpetualLicense).toBe(false) }) }) describe('Grace period handling', () => { it('returns "licensed" for license 0-30 days past expiry', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ isAnnualLicense: true, isAnnualLicenseExpired: false, // Still within 30-day grace period daysSinceExpiry: 20, // 20 days past expiry isInternalLicense: false, }) expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('licensed') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Your tldraw license has expired.', 'License expired 20 days ago.', 'Please reach out to sales@tldraw.com to renew your license.', ]) }) }) describe('Expired licenses', () => { it('returns "expired" for license 30+ days past expiry', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ isAnnualLicense: true, isAnnualLicenseExpired: true, // Beyond 30-day grace period daysSinceExpiry: 35, // 35 days past expiry isInternalLicense: false, }) expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Your tldraw license has been expired for more than 30 days!', 'Please reach out to sales@tldraw.com to renew your license.', ]) }) it('returns "expired" for expired annual license even in dev mode', () => { const licenseResult = getDefaultLicenseResult({ isAnnualLicense: true, isAnnualLicenseExpired: true, isDevelopment: true, isInternalLicense: false, }) expect(getLicenseState(licenseResult, () => {}, true)).toBe('expired') }) it('returns "expired" for expired perpetual license', () => { const licenseResult = getDefaultLicenseResult({ isPerpetualLicense: true, isPerpetualLicenseExpired: true, isInternalLicense: false, }) expect(getLicenseState(licenseResult, () => {}, false)).toBe('expired') }) it('returns "expired" for expired evaluation license', () => { const messages: string[][] = [] const licenseResult = getDefaultLicenseResult({ isEvaluationLicense: true, isEvaluationLicenseExpired: true, isAnnualLicense: false, isPerpetualLicense: false, }) expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Your tldraw evaluation license has expired!', 'Please reach out to sales@tldraw.com to purchase a full license.', ]) }) it('returns "expired" for expired internal annual license with valid domain', () => { const messages: string[][] = [] const expiryDate = new Date(2023, 1, 1) const licenseResult = getDefaultLicenseResult({ isAnnualLicense: true, isAnnualLicenseExpired: true, isInternalLicense: true, isDomainValid: true, expiryDate, }) expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Your tldraw license has been expired for more than 30 days!', 'Please reach out to sales@tldraw.com to renew your license.', ]) }) it('returns "expired" for expired internal perpetual license with valid domain', () => { const messages: string[][] = [] const expiryDate = new Date(2023, 1, 1) const licenseResult = getDefaultLicenseResult({ isPerpetualLicense: true, isPerpetualLicenseExpired: true, isInternalLicense: true, isDomainValid: true, expiryDate, }) expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired') expect(messages).toHaveLength(1) expect(messages[0]).toEqual([ 'Your tldraw license has been expired for more than 30 days!', 'Please reach out to sales@tldraw.com to renew your license.', ]) }) }) })