@tldraw/editor 3.16.0-canary.e9c30b532b82 → 3.16.0-canary.ea008b31887f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist-cjs/index.d.ts +71 -101
  2. package/dist-cjs/index.js +3 -5
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +6 -6
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +7 -10
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  12. package/dist-cjs/lib/config/TLUserPreferences.js +1 -1
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/editor/Editor.js +44 -112
  15. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  16. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  17. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -1
  19. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  20. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +23 -0
  21. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  22. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  23. package/dist-cjs/lib/exports/getSvgJsx.js +34 -14
  24. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  25. package/dist-cjs/lib/hooks/useCanvasEvents.js +7 -5
  26. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  27. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  28. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  29. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  30. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  31. package/dist-cjs/lib/license/LicenseManager.js +138 -50
  32. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  33. package/dist-cjs/lib/license/LicenseProvider.js +39 -1
  34. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  35. package/dist-cjs/lib/license/Watermark.js +68 -6
  36. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  37. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  38. package/dist-cjs/lib/options.js +6 -0
  39. package/dist-cjs/lib/options.js.map +2 -2
  40. package/dist-cjs/lib/primitives/Box.js +3 -0
  41. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  42. package/dist-cjs/lib/primitives/Vec.js +0 -4
  43. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  44. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
  45. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
  47. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  48. package/dist-cjs/lib/utils/reparenting.js +2 -35
  49. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  50. package/dist-cjs/version.js +3 -3
  51. package/dist-cjs/version.js.map +1 -1
  52. package/dist-esm/index.d.mts +71 -101
  53. package/dist-esm/index.mjs +3 -5
  54. package/dist-esm/index.mjs.map +2 -2
  55. package/dist-esm/lib/TldrawEditor.mjs +6 -6
  56. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  57. package/dist-esm/lib/components/Shape.mjs +7 -10
  58. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  59. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
  60. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  62. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  63. package/dist-esm/lib/config/TLUserPreferences.mjs +1 -1
  64. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  65. package/dist-esm/lib/editor/Editor.mjs +44 -112
  66. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  67. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  68. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  69. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -1
  70. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  71. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +23 -0
  72. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  73. package/dist-esm/lib/exports/getSvgJsx.mjs +34 -14
  74. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  75. package/dist-esm/lib/hooks/useCanvasEvents.mjs +7 -5
  76. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  77. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  78. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  79. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  80. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  81. package/dist-esm/lib/license/LicenseManager.mjs +139 -51
  82. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  83. package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
  84. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  85. package/dist-esm/lib/license/Watermark.mjs +68 -6
  86. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  87. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  88. package/dist-esm/lib/options.mjs +6 -0
  89. package/dist-esm/lib/options.mjs.map +2 -2
  90. package/dist-esm/lib/primitives/Box.mjs +4 -1
  91. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  92. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  93. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
  95. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  96. package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
  97. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  98. package/dist-esm/lib/utils/reparenting.mjs +3 -40
  99. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  100. package/dist-esm/version.mjs +3 -3
  101. package/dist-esm/version.mjs.map +1 -1
  102. package/editor.css +16 -3
  103. package/package.json +14 -37
  104. package/src/index.ts +2 -9
  105. package/src/lib/TldrawEditor.tsx +7 -14
  106. package/src/lib/components/Shape.tsx +6 -12
  107. package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
  108. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  109. package/src/lib/config/TLUserPreferences.ts +1 -1
  110. package/src/lib/editor/Editor.test.ts +12 -11
  111. package/src/lib/editor/Editor.ts +53 -149
  112. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  113. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  114. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  115. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  116. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  117. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  118. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  119. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  120. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  121. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  122. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -26
  123. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -1
  124. package/src/lib/editor/shapes/ShapeUtil.ts +46 -0
  125. package/src/lib/editor/types/misc-types.ts +0 -6
  126. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  127. package/src/lib/exports/getSvgJsx.tsx +76 -19
  128. package/src/lib/hooks/useCanvasEvents.ts +6 -6
  129. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  130. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  131. package/src/lib/license/LicenseManager.test.ts +692 -383
  132. package/src/lib/license/LicenseManager.ts +197 -53
  133. package/src/lib/license/LicenseProvider.tsx +74 -2
  134. package/src/lib/license/Watermark.test.tsx +2 -1
  135. package/src/lib/license/Watermark.tsx +73 -6
  136. package/src/lib/license/useLicenseManagerState.ts +2 -2
  137. package/src/lib/options.ts +6 -0
  138. package/src/lib/primitives/Box.test.ts +126 -0
  139. package/src/lib/primitives/Box.ts +10 -1
  140. package/src/lib/primitives/Vec.ts +0 -5
  141. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  142. package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
  143. package/src/lib/primitives/geometry/Group2d.ts +10 -1
  144. package/src/lib/utils/reparenting.ts +3 -69
  145. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  146. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  147. package/src/version.ts +3 -3
  148. package/dist-cjs/lib/utils/nearestMultiple.js +0 -34
  149. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  150. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  151. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  152. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -1,16 +1,18 @@
1
1
  import crypto from 'crypto'
2
+ import { vi } from 'vitest'
2
3
  import { publishDates } from '../../version'
3
4
  import { str2ab } from '../utils/licensing'
4
5
  import {
5
6
  FLAGS,
6
- isEditorUnlicensed,
7
+ getLicenseState,
7
8
  LicenseManager,
8
9
  PROPERTIES,
9
10
  ValidLicenseKeyResult,
10
11
  } from './LicenseManager'
11
12
 
12
- jest.mock('../../version', () => {
13
+ vi.mock('../../version', () => {
13
14
  return {
15
+ version: '3.15.1',
14
16
  publishDates: {
15
17
  major: '2024-06-28T10:56:07.893Z',
16
18
  minor: '2024-07-02T16:49:50.397Z',
@@ -50,337 +52,464 @@ describe('LicenseManager', () => {
50
52
  window.location = new URL('https://www.example.com')
51
53
  })
52
54
 
53
- it('Fails if no key provided', async () => {
54
- const result = await licenseManager.getLicenseFromKey('')
55
- expect(result).toMatchObject({ isLicenseParseable: false, reason: 'no-key-provided' })
56
- })
55
+ describe('Basic license validation', () => {
56
+ it('Fails if no key provided', async () => {
57
+ const result = await licenseManager.getLicenseFromKey('')
58
+ expect(result).toMatchObject({ isLicenseParseable: false, reason: 'no-key-provided' })
59
+ })
57
60
 
58
- it('Signals that it is development mode when appropriate', async () => {
59
- const schemes = ['http', 'https']
60
- for (const scheme of schemes) {
61
- // @ts-ignore
62
- delete window.location
63
- // @ts-ignore
64
- window.location = new URL(`${scheme}://localhost:3000`)
61
+ it('Signals that it is development mode when appropriate', async () => {
62
+ const schemes = ['http', 'https']
63
+ for (const scheme of schemes) {
64
+ // @ts-ignore
65
+ delete window.location
66
+ // @ts-ignore
67
+ window.location = new URL(`${scheme}://localhost:3000`)
68
+
69
+ const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey, 'development')
70
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
71
+ const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
72
+ expect(result).toMatchObject({
73
+ isLicenseParseable: true,
74
+ isDomainValid: false,
75
+ isDevelopment: true,
76
+ })
77
+ }
78
+ })
79
+
80
+ it('Cleanses out valid keys that accidentally have zero-width characters or newlines', async () => {
81
+ const cleanLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
82
+ const dirtyLicenseKey = cleanLicenseKey + '\u200B\u200D\uFEFF\n\r'
83
+ const result = await licenseManager.getLicenseFromKey(dirtyLicenseKey)
84
+ expect(result.isLicenseParseable).toBe(true)
85
+ })
86
+
87
+ it('Fails if garbage key provided', async () => {
88
+ const badPublicKeyLicenseManager = new LicenseManager('', 'badpublickey', 'production')
89
+ const invalidLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
90
+ const result = await badPublicKeyLicenseManager.getLicenseFromKey(invalidLicenseKey)
91
+ expect(result).toMatchObject({ isLicenseParseable: false, reason: 'invalid-license-key' })
92
+ })
93
+
94
+ it('Fails if non-JSON parseable message is provided', async () => {
95
+ const invalidMessage = await generateLicenseKey('asdfsad', keyPair)
96
+ const result = await licenseManager.getLicenseFromKey(invalidMessage)
97
+ expect(result).toMatchObject({ isLicenseParseable: false, reason: 'invalid-license-key' })
98
+ })
65
99
 
66
- const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey, 'development')
100
+ it('Succeeds if valid key provided', async () => {
67
101
  const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
68
- const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
102
+ const result = await licenseManager.getLicenseFromKey(licenseKey)
69
103
  expect(result).toMatchObject({
70
104
  isLicenseParseable: true,
71
- isDomainValid: false,
72
- isDevelopment: true,
73
- })
74
- }
105
+ license: {
106
+ id: 'id',
107
+ hosts: ['www.example.com'],
108
+ flags: FLAGS.ANNUAL_LICENSE,
109
+ expiryDate,
110
+ },
111
+ isDomainValid: true,
112
+ isAnnualLicense: true,
113
+ isAnnualLicenseExpired: false,
114
+ isPerpetualLicense: false,
115
+ isPerpetualLicenseExpired: false,
116
+ isInternalLicense: false,
117
+ isEvaluationLicense: false,
118
+ isEvaluationLicenseExpired: false,
119
+ daysSinceExpiry: 0,
120
+ } as ValidLicenseKeyResult)
121
+ })
75
122
  })
76
123
 
77
- it('Cleanses out valid keys that accidentally have zero-width characters or newlines', async () => {
78
- const cleanLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
79
- const dirtyLicenseKey = cleanLicenseKey + '\u200B\u200D\uFEFF\n\r'
80
- const result = await licenseManager.getLicenseFromKey(dirtyLicenseKey)
81
- expect(result.isLicenseParseable).toBe(true)
82
- })
124
+ describe('Domain validation', () => {
125
+ it('Fails with invalid host', async () => {
126
+ // @ts-ignore
127
+ delete window.location
128
+ // @ts-ignore
129
+ window.location = new URL('https://www.foo.com')
83
130
 
84
- it('Fails if garbage key provided', async () => {
85
- const badPublicKeyLicenseManager = new LicenseManager('', 'badpublickey', 'production')
86
- const invalidLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
87
- const result = await badPublicKeyLicenseManager.getLicenseFromKey(invalidLicenseKey)
88
- expect(result).toMatchObject({ isLicenseParseable: false, reason: 'invalid-license-key' })
89
- })
131
+ const expiredLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
132
+ const result = (await licenseManager.getLicenseFromKey(
133
+ expiredLicenseKey
134
+ )) as ValidLicenseKeyResult
135
+ expect(result.isDomainValid).toBe(false)
136
+ })
90
137
 
91
- it('Fails if non-JSON parseable message is provided', async () => {
92
- const invalidMessage = await generateLicenseKey('asdfsad', keyPair)
93
- const result = await licenseManager.getLicenseFromKey(invalidMessage)
94
- expect(result).toMatchObject({ isLicenseParseable: false, reason: 'invalid-license-key' })
95
- })
138
+ it('Succeeds if hosts is equal to only "*"', async () => {
139
+ // @ts-ignore
140
+ delete window.location
141
+ // @ts-ignore
142
+ window.location = new URL('https://www.foo.com')
143
+
144
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
145
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*']
146
+ const permissiveLicenseKey = await generateLicenseKey(
147
+ JSON.stringify(permissiveHostsInfo),
148
+ keyPair
149
+ )
150
+ const result = (await licenseManager.getLicenseFromKey(
151
+ permissiveLicenseKey
152
+ )) as ValidLicenseKeyResult
153
+ expect(result.isDomainValid).toBe(true)
154
+ })
96
155
 
97
- it('Succeeds if valid key provided', async () => {
98
- const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
99
- const result = await licenseManager.getLicenseFromKey(licenseKey)
100
- expect(result).toMatchObject({
101
- isLicenseParseable: true,
102
- license: {
103
- id: 'id',
104
- hosts: ['www.example.com'],
105
- flags: FLAGS.ANNUAL_LICENSE,
106
- expiryDate,
107
- },
108
- isDomainValid: true,
109
- isAnnualLicense: true,
110
- isAnnualLicenseExpired: false,
111
- isPerpetualLicense: false,
112
- isPerpetualLicenseExpired: false,
113
- isInternalLicense: false,
114
- } as ValidLicenseKeyResult)
115
- })
156
+ it('Succeeds if has an apex domain specified', async () => {
157
+ // @ts-ignore
158
+ delete window.location
159
+ // @ts-ignore
160
+ window.location = new URL('https://www.example.com')
161
+
162
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
163
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['example.com']
164
+ const permissiveLicenseKey = await generateLicenseKey(
165
+ JSON.stringify(permissiveHostsInfo),
166
+ keyPair
167
+ )
168
+ const result = (await licenseManager.getLicenseFromKey(
169
+ permissiveLicenseKey
170
+ )) as ValidLicenseKeyResult
171
+ expect(result.isDomainValid).toBe(true)
172
+ })
116
173
 
117
- it('Fails if the license key has expired', async () => {
118
- const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
119
- const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6) // 6 days ago
120
- expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiryDate
121
- const expiredLicenseKey = await generateLicenseKey(JSON.stringify(expiredLicenseInfo), keyPair)
122
- const result = (await licenseManager.getLicenseFromKey(
123
- expiredLicenseKey
124
- )) as ValidLicenseKeyResult
125
- expect(result.isAnnualLicenseExpired).toBe(true)
126
- })
174
+ it('Succeeds if has an www domain specified, but at the apex domain', async () => {
175
+ // @ts-ignore
176
+ delete window.location
177
+ // @ts-ignore
178
+ window.location = new URL('https://example.com')
179
+
180
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
181
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['www.example.com']
182
+ const permissiveLicenseKey = await generateLicenseKey(
183
+ JSON.stringify(permissiveHostsInfo),
184
+ keyPair
185
+ )
186
+ const result = (await licenseManager.getLicenseFromKey(
187
+ permissiveLicenseKey
188
+ )) as ValidLicenseKeyResult
189
+ expect(result.isDomainValid).toBe(true)
190
+ })
127
191
 
128
- it('Allows a grace period for expired licenses', async () => {
129
- const almostExpiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
130
- const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 5) // 5 days ago
131
- almostExpiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiryDate
132
- const almostExpiredLicenseKey = await generateLicenseKey(
133
- JSON.stringify(almostExpiredLicenseInfo),
134
- keyPair
135
- )
136
- const result = (await licenseManager.getLicenseFromKey(
137
- almostExpiredLicenseKey
138
- )) as ValidLicenseKeyResult
139
- expect(result.isAnnualLicenseExpired).toBe(false)
140
- })
192
+ it('Succeeds if has a subdomain wildcard', async () => {
193
+ // @ts-ignore
194
+ delete window.location
195
+ // @ts-ignore
196
+ window.location = new URL('https://sub.example.com')
197
+
198
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
199
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
200
+ const permissiveLicenseKey = await generateLicenseKey(
201
+ JSON.stringify(permissiveHostsInfo),
202
+ keyPair
203
+ )
204
+ const result = (await licenseManager.getLicenseFromKey(
205
+ permissiveLicenseKey
206
+ )) as ValidLicenseKeyResult
207
+ expect(result.isDomainValid).toBe(true)
208
+ })
141
209
 
142
- // We mock the patch version to be in 2030 above.
143
- it('Succeeds for perpetual license with correct version (and patch does not matter)', async () => {
144
- const majorDate = new Date(publishDates.major)
145
- const expiryDate = new Date(
146
- majorDate.getFullYear(),
147
- majorDate.getMonth(),
148
- majorDate.getDate() + 100
149
- )
150
- const perpetualLicenseInfo = ['id', ['www.example.com'], FLAGS.PERPETUAL_LICENSE, expiryDate]
151
- const almostExpiredLicenseKey = await generateLicenseKey(
152
- JSON.stringify(perpetualLicenseInfo),
153
- keyPair
154
- )
155
- const result = (await licenseManager.getLicenseFromKey(
156
- almostExpiredLicenseKey
157
- )) as ValidLicenseKeyResult
158
- expect(result.isPerpetualLicense).toBe(true)
159
- expect(result.isPerpetualLicenseExpired).toBe(false)
160
- })
210
+ it('Succeeds if has a sub-subdomain wildcard', async () => {
211
+ // @ts-ignore
212
+ delete window.location
213
+ // @ts-ignore
214
+ window.location = new URL('https://pr-2408.sub.example.com')
215
+
216
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
217
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
218
+ const permissiveLicenseKey = await generateLicenseKey(
219
+ JSON.stringify(permissiveHostsInfo),
220
+ keyPair
221
+ )
222
+ const result = (await licenseManager.getLicenseFromKey(
223
+ permissiveLicenseKey
224
+ )) as ValidLicenseKeyResult
225
+ expect(result.isDomainValid).toBe(true)
226
+ })
161
227
 
162
- it('Fails for perpetual license past the release version', async () => {
163
- const majorDate = new Date(publishDates.major)
164
- const expiryDate = new Date(
165
- majorDate.getFullYear(),
166
- majorDate.getMonth(),
167
- majorDate.getDate() - 100
168
- )
169
- const perpetualLicenseInfo = ['id', ['www.example.com'], FLAGS.PERPETUAL_LICENSE, expiryDate]
170
- const almostExpiredLicenseKey = await generateLicenseKey(
171
- JSON.stringify(perpetualLicenseInfo),
172
- keyPair
173
- )
174
- const result = (await licenseManager.getLicenseFromKey(
175
- almostExpiredLicenseKey
176
- )) as ValidLicenseKeyResult
177
- expect(result.isPerpetualLicense).toBe(true)
178
- expect(result.isPerpetualLicenseExpired).toBe(true)
179
- })
228
+ it('Succeeds if has a subdomain wildcard but on an apex domain', async () => {
229
+ // @ts-ignore
230
+ delete window.location
231
+ // @ts-ignore
232
+ window.location = new URL('https://example.com')
233
+
234
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
235
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
236
+ const permissiveLicenseKey = await generateLicenseKey(
237
+ JSON.stringify(permissiveHostsInfo),
238
+ keyPair
239
+ )
240
+ const result = (await licenseManager.getLicenseFromKey(
241
+ permissiveLicenseKey
242
+ )) as ValidLicenseKeyResult
243
+ expect(result.isDomainValid).toBe(true)
244
+ })
180
245
 
181
- it('Fails with invalid host', async () => {
182
- // @ts-ignore
183
- delete window.location
184
- // @ts-ignore
185
- window.location = new URL('https://www.foo.com')
246
+ it('Fails if has a subdomain wildcard isnt for the same base domain', async () => {
247
+ // @ts-ignore
248
+ delete window.location
249
+ // @ts-ignore
250
+ window.location = new URL('https://sub.example.com')
251
+
252
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
253
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.foo.com']
254
+ const permissiveLicenseKey = await generateLicenseKey(
255
+ JSON.stringify(permissiveHostsInfo),
256
+ keyPair
257
+ )
258
+ const result = (await licenseManager.getLicenseFromKey(
259
+ permissiveLicenseKey
260
+ )) as ValidLicenseKeyResult
261
+ expect(result.isDomainValid).toBe(false)
262
+ })
186
263
 
187
- const expiredLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
188
- const result = (await licenseManager.getLicenseFromKey(
189
- expiredLicenseKey
190
- )) as ValidLicenseKeyResult
191
- expect(result.isDomainValid).toBe(false)
192
- })
264
+ it('Succeeds if it is a vscode extension', async () => {
265
+ // @ts-ignore
266
+ delete window.location
267
+ // @ts-ignore
268
+ window.location = new URL(
269
+ 'vscode-webview: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'
270
+ )
271
+
272
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
273
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['tldraw-org.tldraw-vscode']
274
+ const permissiveLicenseKey = await generateLicenseKey(
275
+ JSON.stringify(permissiveHostsInfo),
276
+ keyPair
277
+ )
278
+ const result = (await licenseManager.getLicenseFromKey(
279
+ permissiveLicenseKey
280
+ )) as ValidLicenseKeyResult
281
+ expect(result.isDomainValid).toBe(true)
282
+ })
193
283
 
194
- it('Succeeds if hosts is equal to only "*"', async () => {
195
- // @ts-ignore
196
- delete window.location
197
- // @ts-ignore
198
- window.location = new URL('https://www.foo.com')
199
-
200
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
201
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['*']
202
- const permissiveLicenseKey = await generateLicenseKey(
203
- JSON.stringify(permissiveHostsInfo),
204
- keyPair
205
- )
206
- const result = (await licenseManager.getLicenseFromKey(
207
- permissiveLicenseKey
208
- )) as ValidLicenseKeyResult
209
- expect(result.isDomainValid).toBe(true)
210
- })
284
+ it('Fails if it is a vscode extension with the wrong id', async () => {
285
+ // @ts-ignore
286
+ delete window.location
287
+ // @ts-ignore
288
+ window.location = new URL(
289
+ 'vscode-webview: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'
290
+ )
291
+
292
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
293
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['blah-org.blah-vscode']
294
+ const permissiveLicenseKey = await generateLicenseKey(
295
+ JSON.stringify(permissiveHostsInfo),
296
+ keyPair
297
+ )
298
+ const result = (await licenseManager.getLicenseFromKey(
299
+ permissiveLicenseKey
300
+ )) as ValidLicenseKeyResult
301
+ expect(result.isDomainValid).toBe(false)
302
+ })
211
303
 
212
- it('Succeeds if has an apex domain specified', async () => {
213
- // @ts-ignore
214
- delete window.location
215
- // @ts-ignore
216
- window.location = new URL('https://www.example.com')
304
+ it('Succeeds if it is a native app', async () => {
305
+ // @ts-ignore
306
+ delete window.location
307
+ // @ts-ignore
308
+ window.location = new URL('app-bundle://app/index.html')
309
+
310
+ const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
311
+ nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
312
+ nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
313
+ const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
314
+ const result = (await licenseManager.getLicenseFromKey(
315
+ nativeLicenseKey
316
+ )) as ValidLicenseKeyResult
317
+ expect(result.isDomainValid).toBe(true)
318
+ })
217
319
 
218
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
219
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['example.com']
220
- const permissiveLicenseKey = await generateLicenseKey(
221
- JSON.stringify(permissiveHostsInfo),
222
- keyPair
223
- )
224
- const result = (await licenseManager.getLicenseFromKey(
225
- permissiveLicenseKey
226
- )) as ValidLicenseKeyResult
227
- expect(result.isDomainValid).toBe(true)
320
+ it('Fails if it is a native app with the wrong protocol', async () => {
321
+ // @ts-ignore
322
+ delete window.location
323
+ // @ts-ignore
324
+ window.location = new URL('blah-blundle://app/index.html')
325
+
326
+ const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
327
+ nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
328
+ nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
329
+ const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
330
+ const result = (await licenseManager.getLicenseFromKey(
331
+ nativeLicenseKey
332
+ )) as ValidLicenseKeyResult
333
+ expect(result.isDomainValid).toBe(false)
334
+ })
228
335
  })
229
336
 
230
- it('Succeeds if has an www domain specified, but at the apex domain', async () => {
231
- // @ts-ignore
232
- delete window.location
233
- // @ts-ignore
234
- window.location = new URL('https://example.com')
235
-
236
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
237
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['www.example.com']
238
- const permissiveLicenseKey = await generateLicenseKey(
239
- JSON.stringify(permissiveHostsInfo),
240
- keyPair
241
- )
242
- const result = (await licenseManager.getLicenseFromKey(
243
- permissiveLicenseKey
244
- )) as ValidLicenseKeyResult
245
- expect(result.isDomainValid).toBe(true)
246
- })
337
+ describe('License types and flags', () => {
338
+ it('Checks for internal license', async () => {
339
+ const internalLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
340
+ internalLicenseInfo[PROPERTIES.FLAGS] = FLAGS.INTERNAL_LICENSE
341
+ const internalLicenseKey = await generateLicenseKey(
342
+ JSON.stringify(internalLicenseInfo),
343
+ keyPair
344
+ )
345
+ const result = (await licenseManager.getLicenseFromKey(
346
+ internalLicenseKey
347
+ )) as ValidLicenseKeyResult
348
+ expect(result.isInternalLicense).toBe(true)
349
+ })
247
350
 
248
- it('Succeeds if has a subdomain wildcard', async () => {
249
- // @ts-ignore
250
- delete window.location
251
- // @ts-ignore
252
- window.location = new URL('https://sub.example.com')
253
-
254
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
255
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
256
- const permissiveLicenseKey = await generateLicenseKey(
257
- JSON.stringify(permissiveHostsInfo),
258
- keyPair
259
- )
260
- const result = (await licenseManager.getLicenseFromKey(
261
- permissiveLicenseKey
262
- )) as ValidLicenseKeyResult
263
- expect(result.isDomainValid).toBe(true)
264
- })
351
+ it('Checks for native license', async () => {
352
+ const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
353
+ nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
354
+ const nativeLicenseKey = await generateLicenseKey(JSON.stringify(nativeLicenseInfo), keyPair)
265
355
 
266
- it('Succeeds if has a sub-subdomain wildcard', async () => {
267
- // @ts-ignore
268
- delete window.location
269
- // @ts-ignore
270
- window.location = new URL('https://pr-2408.sub.example.com')
271
-
272
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
273
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
274
- const permissiveLicenseKey = await generateLicenseKey(
275
- JSON.stringify(permissiveHostsInfo),
276
- keyPair
277
- )
278
- const result = (await licenseManager.getLicenseFromKey(
279
- permissiveLicenseKey
280
- )) as ValidLicenseKeyResult
281
- expect(result.isDomainValid).toBe(true)
282
- })
356
+ const result = (await licenseManager.getLicenseFromKey(
357
+ nativeLicenseKey
358
+ )) as ValidLicenseKeyResult
359
+ expect(result.isNativeLicense).toBe(true)
360
+ })
283
361
 
284
- it('Succeeds if has a subdomain wildcard but on an apex domain', async () => {
285
- // @ts-ignore
286
- delete window.location
287
- // @ts-ignore
288
- window.location = new URL('https://example.com')
289
-
290
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
291
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
292
- const permissiveLicenseKey = await generateLicenseKey(
293
- JSON.stringify(permissiveHostsInfo),
294
- keyPair
295
- )
296
- const result = (await licenseManager.getLicenseFromKey(
297
- permissiveLicenseKey
298
- )) as ValidLicenseKeyResult
299
- expect(result.isDomainValid).toBe(true)
300
- })
362
+ it('Checks for license with watermark', async () => {
363
+ const withWatermarkLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
364
+ withWatermarkLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.WITH_WATERMARK
365
+ const withWatermarkLicenseKey = await generateLicenseKey(
366
+ JSON.stringify(withWatermarkLicenseInfo),
367
+ keyPair
368
+ )
369
+ const result = (await licenseManager.getLicenseFromKey(
370
+ withWatermarkLicenseKey
371
+ )) as ValidLicenseKeyResult
372
+ expect(result.isLicensedWithWatermark).toBe(true)
373
+ })
301
374
 
302
- it('Fails if has a subdomain wildcard isnt for the same base domain', async () => {
303
- // @ts-ignore
304
- delete window.location
305
- // @ts-ignore
306
- window.location = new URL('https://sub.example.com')
307
-
308
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
309
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.foo.com']
310
- const permissiveLicenseKey = await generateLicenseKey(
311
- JSON.stringify(permissiveHostsInfo),
312
- keyPair
313
- )
314
- const result = (await licenseManager.getLicenseFromKey(
315
- permissiveLicenseKey
316
- )) as ValidLicenseKeyResult
317
- expect(result.isDomainValid).toBe(false)
318
- })
375
+ it('Checks for evaluation license', async () => {
376
+ const evaluationLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
377
+ evaluationLicenseInfo[PROPERTIES.FLAGS] = FLAGS.EVALUATION_LICENSE
378
+ const evaluationLicenseKey = await generateLicenseKey(
379
+ JSON.stringify(evaluationLicenseInfo),
380
+ keyPair
381
+ )
382
+ const result = (await licenseManager.getLicenseFromKey(
383
+ evaluationLicenseKey
384
+ )) as ValidLicenseKeyResult
385
+ expect(result.isEvaluationLicense).toBe(true)
386
+ expect(result.isEvaluationLicenseExpired).toBe(false)
387
+ })
319
388
 
320
- it('Succeeds if it is a vscode extension', async () => {
321
- // @ts-ignore
322
- delete window.location
323
- // @ts-ignore
324
- window.location = new URL(
325
- 'vscode-webview: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'
326
- )
327
-
328
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
329
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['tldraw-org.tldraw-vscode']
330
- const permissiveLicenseKey = await generateLicenseKey(
331
- JSON.stringify(permissiveHostsInfo),
332
- keyPair
333
- )
334
- const result = (await licenseManager.getLicenseFromKey(
335
- permissiveLicenseKey
336
- )) as ValidLicenseKeyResult
337
- expect(result.isDomainValid).toBe(true)
389
+ it('Detects when evaluation license has expired', async () => {
390
+ const expiredEvaluationLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
391
+ expiredEvaluationLicenseInfo[PROPERTIES.FLAGS] = FLAGS.EVALUATION_LICENSE
392
+ const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1) // 1 day ago
393
+ expiredEvaluationLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString()
394
+
395
+ const expiredEvaluationLicenseKey = await generateLicenseKey(
396
+ JSON.stringify(expiredEvaluationLicenseInfo),
397
+ keyPair
398
+ )
399
+
400
+ // The getLicenseFromKey should return the expired state
401
+ const result = (await licenseManager.getLicenseFromKey(
402
+ expiredEvaluationLicenseKey
403
+ )) as ValidLicenseKeyResult
404
+ expect(result.isEvaluationLicense).toBe(true)
405
+ expect(result.isEvaluationLicenseExpired).toBe(true)
406
+ })
338
407
  })
339
408
 
340
- it('Fails if it is a vscode extension with the wrong id', async () => {
341
- // @ts-ignore
342
- delete window.location
343
- // @ts-ignore
344
- window.location = new URL(
345
- 'vscode-webview: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'
346
- )
347
-
348
- const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
349
- permissiveHostsInfo[PROPERTIES.HOSTS] = ['blah-org.blah-vscode']
350
- const permissiveLicenseKey = await generateLicenseKey(
351
- JSON.stringify(permissiveHostsInfo),
352
- keyPair
353
- )
354
- const result = (await licenseManager.getLicenseFromKey(
355
- permissiveLicenseKey
356
- )) as ValidLicenseKeyResult
357
- expect(result.isDomainValid).toBe(false)
358
- })
409
+ describe('License expiry and grace period', () => {
410
+ it('Fails if the license key has expired beyond grace period', async () => {
411
+ const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
412
+ const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 40) // 40 days ago (beyond 30-day grace period)
413
+ expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiryDate
414
+ const expiredLicenseKey = await generateLicenseKey(
415
+ JSON.stringify(expiredLicenseInfo),
416
+ keyPair
417
+ )
418
+ const result = (await licenseManager.getLicenseFromKey(
419
+ expiredLicenseKey
420
+ )) as ValidLicenseKeyResult
421
+ expect(result.isAnnualLicenseExpired).toBe(true)
422
+ })
423
+
424
+ it('Allows a grace period for expired licenses', async () => {
425
+ const almostExpiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
426
+ const expiryDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 5) // 5 days ago
427
+ almostExpiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiryDate
428
+ const almostExpiredLicenseKey = await generateLicenseKey(
429
+ JSON.stringify(almostExpiredLicenseInfo),
430
+ keyPair
431
+ )
432
+ const result = (await licenseManager.getLicenseFromKey(
433
+ almostExpiredLicenseKey
434
+ )) as ValidLicenseKeyResult
435
+ expect(result.isAnnualLicenseExpired).toBe(false)
436
+ })
359
437
 
360
- it('Checks for internal license', async () => {
361
- const internalLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
362
- internalLicenseInfo[PROPERTIES.FLAGS] = FLAGS.INTERNAL_LICENSE
363
- const internalLicenseKey = await generateLicenseKey(
364
- JSON.stringify(internalLicenseInfo),
365
- keyPair
366
- )
367
- const result = (await licenseManager.getLicenseFromKey(
368
- internalLicenseKey
369
- )) as ValidLicenseKeyResult
370
- expect(result.isInternalLicense).toBe(true)
438
+ it('Handles grace period correctly - 20 days expired should still be within grace period', async () => {
439
+ const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
440
+ const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 20) // 20 days ago
441
+ expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString()
442
+
443
+ const expiredLicenseKey = await generateLicenseKey(
444
+ JSON.stringify(expiredLicenseInfo),
445
+ keyPair
446
+ )
447
+
448
+ // Test the getLicenseFromKey method to verify grace period calculation
449
+ const result = (await licenseManager.getLicenseFromKey(
450
+ expiredLicenseKey
451
+ )) as ValidLicenseKeyResult
452
+ expect(result.isAnnualLicense).toBe(true)
453
+ expect(result.isAnnualLicenseExpired).toBe(false) // Within 30-day grace period
454
+ expect(result.daysSinceExpiry).toBe(20)
455
+ })
456
+
457
+ it('Calculates days since expiry correctly', async () => {
458
+ const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
459
+ const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 15) // 15 days ago
460
+ expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString()
461
+
462
+ const expiredLicenseKey = await generateLicenseKey(
463
+ JSON.stringify(expiredLicenseInfo),
464
+ keyPair
465
+ )
466
+
467
+ const result = (await licenseManager.getLicenseFromKey(
468
+ expiredLicenseKey
469
+ )) as ValidLicenseKeyResult
470
+ expect(result.daysSinceExpiry).toBe(15)
471
+ })
371
472
  })
372
473
 
373
- it('Checks for license with watermark', async () => {
374
- const withWatermarkLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
375
- withWatermarkLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.WITH_WATERMARK
376
- const withWatermarkLicenseKey = await generateLicenseKey(
377
- JSON.stringify(withWatermarkLicenseInfo),
378
- keyPair
379
- )
380
- const result = (await licenseManager.getLicenseFromKey(
381
- withWatermarkLicenseKey
382
- )) as ValidLicenseKeyResult
383
- expect(result.isLicensedWithWatermark).toBe(true)
474
+ describe('Perpetual licenses', () => {
475
+ // We mock the patch version to be in 2030 above.
476
+ it('Succeeds for perpetual license with correct version (and patch does not matter)', async () => {
477
+ const majorDate = new Date(publishDates.major)
478
+ const expiryDate = new Date(
479
+ majorDate.getFullYear(),
480
+ majorDate.getMonth(),
481
+ majorDate.getDate() + 100
482
+ )
483
+ const perpetualLicenseInfo = ['id', ['www.example.com'], FLAGS.PERPETUAL_LICENSE, expiryDate]
484
+ const almostExpiredLicenseKey = await generateLicenseKey(
485
+ JSON.stringify(perpetualLicenseInfo),
486
+ keyPair
487
+ )
488
+ const result = (await licenseManager.getLicenseFromKey(
489
+ almostExpiredLicenseKey
490
+ )) as ValidLicenseKeyResult
491
+ expect(result.isPerpetualLicense).toBe(true)
492
+ expect(result.isPerpetualLicenseExpired).toBe(false)
493
+ })
494
+
495
+ it('Fails for perpetual license past the release version', async () => {
496
+ const majorDate = new Date(publishDates.major)
497
+ const expiryDate = new Date(
498
+ majorDate.getFullYear(),
499
+ majorDate.getMonth(),
500
+ majorDate.getDate() - 100
501
+ )
502
+ const perpetualLicenseInfo = ['id', ['www.example.com'], FLAGS.PERPETUAL_LICENSE, expiryDate]
503
+ const almostExpiredLicenseKey = await generateLicenseKey(
504
+ JSON.stringify(perpetualLicenseInfo),
505
+ keyPair
506
+ )
507
+ const result = (await licenseManager.getLicenseFromKey(
508
+ almostExpiredLicenseKey
509
+ )) as ValidLicenseKeyResult
510
+ expect(result.isPerpetualLicense).toBe(true)
511
+ expect(result.isPerpetualLicenseExpired).toBe(true)
512
+ })
384
513
  })
385
514
  })
386
515
 
@@ -467,12 +596,16 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
467
596
  isAnnualLicense: true,
468
597
  isAnnualLicenseExpired: false,
469
598
  isInternalLicense: false,
599
+ isNativeLicense: false,
470
600
  isDevelopment: false,
471
601
  isDomainValid: true,
472
602
  isPerpetualLicense: false,
473
603
  isPerpetualLicenseExpired: false,
474
604
  isLicenseParseable: true as const,
475
605
  isLicensedWithWatermark: false,
606
+ isEvaluationLicense: false,
607
+ isEvaluationLicenseExpired: false,
608
+ daysSinceExpiry: 0,
476
609
  // WatermarkManager does not check these fields, it relies on the calculated values like isAnnualLicenseExpired
477
610
  license: {
478
611
  id: 'id',
@@ -485,115 +618,291 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
485
618
  }
486
619
  }
487
620
 
488
- describe(isEditorUnlicensed, () => {
489
- it('shows watermark when license is not parseable', () => {
490
- const licenseResult = getDefaultLicenseResult({
491
- // @ts-ignore
492
- isLicenseParseable: false,
621
+ describe('getLicenseState', () => {
622
+ describe('Development mode', () => {
623
+ it('returns "unlicensed" for unparseable license in development', () => {
624
+ const licenseResult = getDefaultLicenseResult({
625
+ // @ts-ignore
626
+ isLicenseParseable: false,
627
+ })
628
+ expect(getLicenseState(licenseResult, () => {}, true)).toBe('unlicensed')
493
629
  })
494
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
495
- })
496
630
 
497
- it('shows watermark when domain is not valid', () => {
498
- const licenseResult = getDefaultLicenseResult({
499
- isDomainValid: false,
631
+ it('returns "licensed" for invalid domain in development mode', () => {
632
+ const licenseResult = getDefaultLicenseResult({
633
+ isDomainValid: false,
634
+ isDevelopment: true,
635
+ })
636
+ expect(getLicenseState(licenseResult, () => {}, true)).toBe('licensed')
500
637
  })
501
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
502
- })
503
638
 
504
- it('shows watermark when annual license has expired', () => {
505
- const licenseResult = getDefaultLicenseResult({
506
- isAnnualLicense: true,
507
- isAnnualLicenseExpired: true,
639
+ it('returns "licensed" for valid license in development mode', () => {
640
+ const licenseResult = getDefaultLicenseResult({
641
+ isDevelopment: true,
642
+ })
643
+ expect(getLicenseState(licenseResult, () => {}, true)).toBe('licensed')
508
644
  })
509
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
510
- })
511
645
 
512
- it('shows watermark when annual license has expired, even if dev mode', () => {
513
- const licenseResult = getDefaultLicenseResult({
514
- isAnnualLicense: true,
515
- isAnnualLicenseExpired: true,
516
- isDevelopment: true,
646
+ it('returns "unlicensed" for no license key in development (localhost)', () => {
647
+ const licenseResult = getDefaultLicenseResult({
648
+ // @ts-ignore
649
+ isLicenseParseable: false,
650
+ reason: 'no-key-provided',
651
+ })
652
+ expect(getLicenseState(licenseResult, () => {}, true)).toBe('unlicensed')
517
653
  })
518
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
519
654
  })
520
655
 
521
- it('shows watermark when perpetual license has expired', () => {
522
- const licenseResult = getDefaultLicenseResult({
523
- isPerpetualLicense: true,
524
- isPerpetualLicenseExpired: true,
656
+ describe('Production mode - unlicensed states', () => {
657
+ it('returns "unlicensed-production" for unparseable license in production (invalid-license-key)', () => {
658
+ const messages: string[][] = []
659
+ const licenseResult = getDefaultLicenseResult({
660
+ // @ts-ignore
661
+ isLicenseParseable: false,
662
+ reason: 'invalid-license-key',
663
+ })
664
+ const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)
665
+ expect(result).toBe('unlicensed-production')
666
+
667
+ expect(messages).toHaveLength(1)
668
+ expect(messages[0]).toEqual([
669
+ 'Invalid license key. tldraw requires a valid license for production use.',
670
+ 'Please reach out to sales@tldraw.com to purchase a license.',
671
+ ])
525
672
  })
526
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
527
- })
528
673
 
529
- it('does not show watermark when license is valid and not expired', () => {
530
- const licenseResult = getDefaultLicenseResult({
531
- isAnnualLicense: true,
532
- isAnnualLicenseExpired: false,
533
- isInternalLicense: false,
674
+ it('returns "unlicensed-production" for no license key in production', () => {
675
+ const messages: string[][] = []
676
+ const licenseResult = getDefaultLicenseResult({
677
+ // @ts-ignore
678
+ isLicenseParseable: false,
679
+ reason: 'no-key-provided',
680
+ })
681
+ const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)
682
+ expect(result).toBe('unlicensed-production')
683
+
684
+ expect(messages).toHaveLength(1)
685
+ expect(messages[0]).toEqual([
686
+ 'No tldraw license key provided!',
687
+ 'A license is required for production deployments.',
688
+ 'Please reach out to sales@tldraw.com to purchase a license.',
689
+ ])
690
+ })
691
+
692
+ it('returns "unlicensed-production" for invalid license key in production', () => {
693
+ const messages: string[][] = []
694
+ const licenseResult = getDefaultLicenseResult({
695
+ // @ts-ignore
696
+ isLicenseParseable: false,
697
+ reason: 'invalid-license-key',
698
+ })
699
+ const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)
700
+ expect(result).toBe('unlicensed-production')
701
+
702
+ expect(messages).toHaveLength(1)
703
+ expect(messages[0]).toEqual([
704
+ 'Invalid license key. tldraw requires a valid license for production use.',
705
+ 'Please reach out to sales@tldraw.com to purchase a license.',
706
+ ])
534
707
  })
535
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
536
- })
537
708
 
538
- it('does not show watermark when perpetual license is valid and not expired', () => {
539
- const licenseResult = getDefaultLicenseResult({
540
- isPerpetualLicense: true,
541
- isPerpetualLicenseExpired: false,
542
- isInternalLicense: false,
709
+ it('returns "unlicensed-production" for invalid domain in production', () => {
710
+ const messages: string[][] = []
711
+ const licenseResult = getDefaultLicenseResult({
712
+ isDomainValid: false,
713
+ isDevelopment: false,
714
+ })
715
+ const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)
716
+ expect(result).toBe('unlicensed-production')
717
+
718
+ expect(messages).toHaveLength(1)
719
+ expect(messages[0]).toEqual([
720
+ 'License key is not valid for this domain.',
721
+ 'A license is required for production deployments.',
722
+ 'Please reach out to sales@tldraw.com to purchase a license.',
723
+ ])
543
724
  })
544
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
545
- })
546
725
 
547
- it('does not show watermark when in development mode', () => {
548
- const licenseResult = getDefaultLicenseResult({
549
- isDevelopment: true,
726
+ it('returns "unlicensed-production" for expired internal license with invalid domain', () => {
727
+ const messages: string[][] = []
728
+ const expiryDate = new Date(2023, 1, 1)
729
+ const licenseResult = getDefaultLicenseResult({
730
+ isAnnualLicense: true,
731
+ isAnnualLicenseExpired: true,
732
+ isInternalLicense: true,
733
+ isDomainValid: false,
734
+ expiryDate,
735
+ })
736
+ const result = getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)
737
+ expect(result).toBe('unlicensed-production')
738
+
739
+ expect(messages).toHaveLength(1)
740
+ expect(messages[0]).toEqual([
741
+ 'License key is not valid for this domain.',
742
+ 'A license is required for production deployments.',
743
+ 'Please reach out to sales@tldraw.com to purchase a license.',
744
+ ])
550
745
  })
551
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
552
746
  })
553
747
 
554
- it('does not show watermark when license is parseable and domain is valid', () => {
555
- const licenseResult = getDefaultLicenseResult({
556
- isLicenseParseable: true,
557
- isDomainValid: true,
748
+ describe('Valid licenses', () => {
749
+ it('returns "licensed" for valid annual license', () => {
750
+ const licenseResult = getDefaultLicenseResult({
751
+ isAnnualLicense: true,
752
+ isAnnualLicenseExpired: false,
753
+ })
754
+ expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed')
558
755
  })
559
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
560
- })
561
756
 
562
- it('does not show watermark when license is parseable and domain is not valid and dev mode', () => {
563
- const licenseResult = getDefaultLicenseResult({
564
- isLicenseParseable: true,
565
- isDomainValid: false,
566
- isDevelopment: true,
757
+ it('returns "licensed" for valid perpetual license', () => {
758
+ const licenseResult = getDefaultLicenseResult({
759
+ isPerpetualLicense: true,
760
+ isPerpetualLicenseExpired: false,
761
+ })
762
+ expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed')
567
763
  })
568
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
569
- })
570
764
 
571
- it('throws when an internal annual license has expired', () => {
572
- const expiryDate = new Date(2023, 1, 1)
573
- const licenseResult = getDefaultLicenseResult({
574
- isAnnualLicense: true,
575
- isAnnualLicenseExpired: true,
576
- isInternalLicense: true,
577
- expiryDate,
765
+ it('returns "licensed-with-watermark" for watermarked license', () => {
766
+ const licenseResult = getDefaultLicenseResult({
767
+ isLicensedWithWatermark: true,
768
+ })
769
+ expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed-with-watermark')
770
+ })
771
+
772
+ it('returns "licensed" for valid evaluation license', () => {
773
+ const licenseResult = getDefaultLicenseResult({
774
+ isEvaluationLicense: true,
775
+ isLicensedWithWatermark: false, // Evaluation license doesn't need WITH_WATERMARK flag
776
+ isAnnualLicense: false,
777
+ isPerpetualLicense: false,
778
+ })
779
+
780
+ // Evaluation license should be licensed but tracked (no watermark shown)
781
+ expect(getLicenseState(licenseResult, () => {}, false)).toBe('licensed')
782
+
783
+ // Verify evaluation license properties
784
+ expect(licenseResult.isEvaluationLicense).toBe(true)
785
+ expect(licenseResult.isLicensedWithWatermark).toBe(false) // No explicit watermark flag needed
786
+ expect(licenseResult.isAnnualLicense).toBe(false)
787
+ expect(licenseResult.isPerpetualLicense).toBe(false)
578
788
  })
579
- expect(() => isEditorUnlicensed(licenseResult)).toThrow(/License: Internal license expired/)
580
789
  })
581
790
 
582
- it('throws when an internal perpetual license has expired', () => {
583
- const expiryDate = new Date(2023, 1, 1)
584
- const licenseResult = getDefaultLicenseResult({
585
- isPerpetualLicense: true,
586
- isPerpetualLicenseExpired: true,
587
- isInternalLicense: true,
588
- expiryDate,
791
+ describe('Grace period handling', () => {
792
+ it('returns "licensed" for license 0-30 days past expiry', () => {
793
+ const messages: string[][] = []
794
+ const licenseResult = getDefaultLicenseResult({
795
+ isAnnualLicense: true,
796
+ isAnnualLicenseExpired: false, // Still within 30-day grace period
797
+ daysSinceExpiry: 20, // 20 days past expiry
798
+ isInternalLicense: false,
799
+ })
800
+
801
+ expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('licensed')
802
+
803
+ expect(messages).toHaveLength(1)
804
+ expect(messages[0]).toEqual([
805
+ 'Your tldraw license has expired.',
806
+ 'License expired 20 days ago.',
807
+ 'Please reach out to sales@tldraw.com to renew your license.',
808
+ ])
589
809
  })
590
- expect(() => isEditorUnlicensed(licenseResult)).toThrow(/License: Internal license expired/)
591
810
  })
592
811
 
593
- it('shows watermark when license has that flag specified', () => {
594
- const licenseResult = getDefaultLicenseResult({
595
- isLicensedWithWatermark: true,
812
+ describe('Expired licenses', () => {
813
+ it('returns "expired" for license 30+ days past expiry', () => {
814
+ const messages: string[][] = []
815
+ const licenseResult = getDefaultLicenseResult({
816
+ isAnnualLicense: true,
817
+ isAnnualLicenseExpired: true, // Beyond 30-day grace period
818
+ daysSinceExpiry: 35, // 35 days past expiry
819
+ isInternalLicense: false,
820
+ })
821
+
822
+ expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired')
823
+
824
+ expect(messages).toHaveLength(1)
825
+ expect(messages[0]).toEqual([
826
+ 'Your tldraw license has been expired for more than 30 days!',
827
+ 'Please reach out to sales@tldraw.com to renew your license.',
828
+ ])
829
+ })
830
+
831
+ it('returns "expired" for expired annual license even in dev mode', () => {
832
+ const licenseResult = getDefaultLicenseResult({
833
+ isAnnualLicense: true,
834
+ isAnnualLicenseExpired: true,
835
+ isDevelopment: true,
836
+ isInternalLicense: false,
837
+ })
838
+ expect(getLicenseState(licenseResult, () => {}, true)).toBe('expired')
839
+ })
840
+
841
+ it('returns "expired" for expired perpetual license', () => {
842
+ const licenseResult = getDefaultLicenseResult({
843
+ isPerpetualLicense: true,
844
+ isPerpetualLicenseExpired: true,
845
+ isInternalLicense: false,
846
+ })
847
+ expect(getLicenseState(licenseResult, () => {}, false)).toBe('expired')
848
+ })
849
+
850
+ it('returns "expired" for expired evaluation license', () => {
851
+ const messages: string[][] = []
852
+ const licenseResult = getDefaultLicenseResult({
853
+ isEvaluationLicense: true,
854
+ isEvaluationLicenseExpired: true,
855
+ isAnnualLicense: false,
856
+ isPerpetualLicense: false,
857
+ })
858
+
859
+ expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired')
860
+
861
+ expect(messages).toHaveLength(1)
862
+ expect(messages[0]).toEqual([
863
+ 'Your tldraw evaluation license has expired!',
864
+ 'Please reach out to sales@tldraw.com to purchase a full license.',
865
+ ])
866
+ })
867
+
868
+ it('returns "expired" for expired internal annual license with valid domain', () => {
869
+ const messages: string[][] = []
870
+ const expiryDate = new Date(2023, 1, 1)
871
+ const licenseResult = getDefaultLicenseResult({
872
+ isAnnualLicense: true,
873
+ isAnnualLicenseExpired: true,
874
+ isInternalLicense: true,
875
+ isDomainValid: true,
876
+ expiryDate,
877
+ })
878
+
879
+ expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired')
880
+
881
+ expect(messages).toHaveLength(1)
882
+ expect(messages[0]).toEqual([
883
+ 'Your tldraw license has been expired for more than 30 days!',
884
+ 'Please reach out to sales@tldraw.com to renew your license.',
885
+ ])
886
+ })
887
+
888
+ it('returns "expired" for expired internal perpetual license with valid domain', () => {
889
+ const messages: string[][] = []
890
+ const expiryDate = new Date(2023, 1, 1)
891
+ const licenseResult = getDefaultLicenseResult({
892
+ isPerpetualLicense: true,
893
+ isPerpetualLicenseExpired: true,
894
+ isInternalLicense: true,
895
+ isDomainValid: true,
896
+ expiryDate,
897
+ })
898
+
899
+ expect(getLicenseState(licenseResult, (msgs) => messages.push(msgs), false)).toBe('expired')
900
+
901
+ expect(messages).toHaveLength(1)
902
+ expect(messages[0]).toEqual([
903
+ 'Your tldraw license has been expired for more than 30 days!',
904
+ 'Please reach out to sales@tldraw.com to renew your license.',
905
+ ])
596
906
  })
597
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
598
907
  })
599
908
  })