@tldraw/editor 3.16.0-canary.f55016ece635 → 3.16.0-canary.f5bf2b535ea7

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