@tldraw/editor 3.16.0-next.fe14f1b4181f → 4.0.1

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