@tldraw/editor 5.3.0 → 5.3.2

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 (44) hide show
  1. package/dist-cjs/index.d.ts +3 -0
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +3 -0
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/editor/Editor.js +8 -0
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/hooks/useDocumentEvents.js +3 -2
  9. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  10. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  11. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  12. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  13. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  14. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  15. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  16. package/dist-cjs/version.js +2 -2
  17. package/dist-cjs/version.js.map +1 -1
  18. package/dist-esm/index.d.mts +3 -0
  19. package/dist-esm/index.mjs +7 -2
  20. package/dist-esm/index.mjs.map +2 -2
  21. package/dist-esm/lib/TldrawEditor.mjs +4 -1
  22. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  23. package/dist-esm/lib/editor/Editor.mjs +8 -0
  24. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  25. package/dist-esm/lib/hooks/useDocumentEvents.mjs +3 -2
  26. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  27. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  28. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  29. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  30. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  31. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  32. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  33. package/dist-esm/version.mjs +2 -2
  34. package/dist-esm/version.mjs.map +1 -1
  35. package/package.json +13 -7
  36. package/src/index.ts +5 -1
  37. package/src/lib/TldrawEditor.tsx +4 -1
  38. package/src/lib/editor/Editor.ts +10 -0
  39. package/src/lib/hooks/useDocumentEvents.ts +9 -8
  40. package/src/lib/license/LicenseManager.test.ts +160 -0
  41. package/src/lib/license/LicenseManager.ts +13 -4
  42. package/src/lib/license/LicenseProvider.tsx +24 -3
  43. package/src/lib/license/useLicenseManagerState.ts +8 -6
  44. package/src/version.ts +2 -2
@@ -316,6 +316,42 @@ describe('LicenseManager', () => {
316
316
  expect(result.isDomainValid).toBe(true)
317
317
  })
318
318
 
319
+ it('Fails if the wildcard domain is a prefix of the current hostname', async () => {
320
+ // @ts-ignore
321
+ delete window.location
322
+ // @ts-ignore
323
+ window.location = new URL('https://sub.example.com.other.io')
324
+
325
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
326
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
327
+ const permissiveLicenseKey = await generateLicenseKey(
328
+ JSON.stringify(permissiveHostsInfo),
329
+ keyPair
330
+ )
331
+ const result = (await licenseManager.getLicenseFromKey(
332
+ permissiveLicenseKey
333
+ )) as ValidLicenseKeyResult
334
+ expect(result.isDomainValid).toBe(false)
335
+ })
336
+
337
+ it('Fails if the current hostname only ends with the wildcard domain', async () => {
338
+ // @ts-ignore
339
+ delete window.location
340
+ // @ts-ignore
341
+ window.location = new URL('https://notexample.com')
342
+
343
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
344
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
345
+ const permissiveLicenseKey = await generateLicenseKey(
346
+ JSON.stringify(permissiveHostsInfo),
347
+ keyPair
348
+ )
349
+ const result = (await licenseManager.getLicenseFromKey(
350
+ permissiveLicenseKey
351
+ )) as ValidLicenseKeyResult
352
+ expect(result.isDomainValid).toBe(false)
353
+ })
354
+
319
355
  it('Fails if has a subdomain wildcard isnt for the same base domain', async () => {
320
356
  // @ts-ignore
321
357
  delete window.location
@@ -422,6 +458,107 @@ describe('LicenseManager', () => {
422
458
  expect(result.isDomainValid).toBe(true)
423
459
  })
424
460
 
461
+ it('Is not development mode on a custom protocol', async () => {
462
+ process.env.NODE_ENV = 'production'
463
+ try {
464
+ // @ts-ignore
465
+ delete window.location
466
+ // @ts-ignore
467
+ window.location = new URL('app-bundle://app/index.html')
468
+
469
+ const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
470
+ nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
471
+ nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
472
+ const nativeLicenseKey = await generateLicenseKey(
473
+ JSON.stringify(nativeLicenseInfo),
474
+ keyPair
475
+ )
476
+ const nativeLicenseManager = new LicenseManager('', keyPair.publicKey)
477
+ const result = (await nativeLicenseManager.getLicenseFromKey(
478
+ nativeLicenseKey
479
+ )) as ValidLicenseKeyResult
480
+ expect(result).toMatchObject({
481
+ isLicenseParseable: true,
482
+ isNativeLicense: true,
483
+ isDomainValid: true,
484
+ isDevelopment: false,
485
+ })
486
+ expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe('licensed')
487
+ } finally {
488
+ process.env.NODE_ENV = 'test'
489
+ }
490
+ })
491
+
492
+ it('Validates the domain on a custom protocol', async () => {
493
+ process.env.NODE_ENV = 'production'
494
+ try {
495
+ // @ts-ignore
496
+ delete window.location
497
+ // @ts-ignore
498
+ window.location = new URL('app-bundle://app/index.html')
499
+
500
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
501
+ const customProtocolLicenseManager = new LicenseManager('', keyPair.publicKey)
502
+ const result = (await customProtocolLicenseManager.getLicenseFromKey(
503
+ licenseKey
504
+ )) as ValidLicenseKeyResult
505
+ expect(result).toMatchObject({ isDomainValid: false, isDevelopment: false })
506
+ expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe(
507
+ 'unlicensed-production'
508
+ )
509
+ } finally {
510
+ process.env.NODE_ENV = 'test'
511
+ }
512
+ })
513
+
514
+ it('Is development mode over http on a non-loopback host', async () => {
515
+ process.env.NODE_ENV = 'production'
516
+ try {
517
+ // @ts-ignore
518
+ delete window.location
519
+ // @ts-ignore
520
+ window.location = new URL('http://www.example.com')
521
+
522
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
523
+ const httpLicenseManager = new LicenseManager('', keyPair.publicKey)
524
+ const result = (await httpLicenseManager.getLicenseFromKey(
525
+ licenseKey
526
+ )) as ValidLicenseKeyResult
527
+ expect(result.isDevelopment).toBe(true)
528
+ } finally {
529
+ process.env.NODE_ENV = 'test'
530
+ }
531
+ })
532
+
533
+ it.each([
534
+ ['http://localhost:5173/', true],
535
+ ['http://127.0.0.1:3000/', true],
536
+ ['https://localhost:8443/', true],
537
+ ['https://[::1]:8443/', true],
538
+ ['http://192.168.1.5:3000/', true],
539
+ ['http://staging-box/', true],
540
+ ['https://www.example.com/', false],
541
+ ['tauri://localhost/index.html', false],
542
+ ['http://tauri.localhost/index.html', false],
543
+ ['app://tauri.localhost/com.example.app', false],
544
+ ['app-bundle://localhost/index.html', false],
545
+ ])('Development mode at %s is %s', async (href, expected) => {
546
+ process.env.NODE_ENV = 'production'
547
+ try {
548
+ // @ts-ignore
549
+ delete window.location
550
+ // @ts-ignore
551
+ window.location = new URL(href)
552
+
553
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
554
+ const manager = new LicenseManager('', keyPair.publicKey)
555
+ const result = (await manager.getLicenseFromKey(licenseKey)) as ValidLicenseKeyResult
556
+ expect(result.isDevelopment).toBe(expected)
557
+ } finally {
558
+ process.env.NODE_ENV = 'test'
559
+ }
560
+ })
561
+
425
562
  it('Fails if it is a native app with the wrong protocol', async () => {
426
563
  // @ts-ignore
427
564
  delete window.location
@@ -437,6 +574,29 @@ describe('LicenseManager', () => {
437
574
  )) as ValidLicenseKeyResult
438
575
  expect(result.isDomainValid).toBe(false)
439
576
  })
577
+
578
+ it.each(['tauri://localhost/index.html', 'http://tauri.localhost/index.html'])(
579
+ 'Does not license an unrelated key at %s',
580
+ async (href) => {
581
+ process.env.NODE_ENV = 'production'
582
+ try {
583
+ // @ts-ignore
584
+ delete window.location
585
+ // @ts-ignore
586
+ window.location = new URL(href)
587
+
588
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
589
+ const manager = new LicenseManager('', keyPair.publicKey)
590
+ const result = (await manager.getLicenseFromKey(licenseKey)) as ValidLicenseKeyResult
591
+ expect(result).toMatchObject({ isDevelopment: false, isDomainValid: false })
592
+ expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe(
593
+ 'unlicensed-production'
594
+ )
595
+ } finally {
596
+ process.env.NODE_ENV = 'test'
597
+ }
598
+ }
599
+ )
440
600
  })
441
601
 
442
602
  describe('License types and flags', () => {
@@ -174,10 +174,17 @@ export class LicenseManager {
174
174
  }
175
175
 
176
176
  private getIsDevelopment() {
177
- // If we are using https on a non-loopback domain we assume it's a production env and a development one otherwise
177
+ const protocol = window.location.protocol
178
+ const hostname = window.location.hostname
179
+
180
+ // Tauri uses `tauri://localhost` on macOS and Linux and `http://tauri.localhost` on Windows.
181
+ if (hostname.toLowerCase().endsWith('.localhost')) {
182
+ return process.env.NODE_ENV !== 'production'
183
+ }
184
+
178
185
  return (
179
- !['https:', 'vscode-webview:'].includes(window.location.protocol) ||
180
- this.isLoopbackHost(window.location.hostname) ||
186
+ protocol === 'http:' ||
187
+ (protocol === 'https:' && this.isLoopbackHost(hostname)) ||
181
188
  process.env.NODE_ENV !== 'production'
182
189
  )
183
190
  }
@@ -407,7 +414,9 @@ export class LicenseManager {
407
414
 
408
415
  // Glob testing, we only support '*.somedomain.com' right now.
409
416
  if (host.includes('*')) {
410
- const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
417
+ const globToRegex = new RegExp(
418
+ normalizedHostOrUrlRegex.replace(/\./g, '\\.').replace(/\*/g, '.*?') + '$'
419
+ )
411
420
  return globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`)
412
421
  }
413
422
 
@@ -1,13 +1,34 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
2
  import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
3
+ import { useMaybeEditor } from '../hooks/useEditor'
3
4
  import { LicenseManager } from './LicenseManager'
4
5
 
5
6
  /** @internal */
6
- export const LicenseContext = createContext({} as LicenseManager)
7
+ export const LicenseContext = createContext<LicenseManager | null>(null)
7
8
 
8
9
  /** @internal */
9
- export function useLicenseContext() {
10
- return useContext(LicenseContext)
10
+ export function useLicenseContext(): LicenseManager {
11
+ const licenseManager = useMaybeLicenseManager()
12
+ if (!licenseManager) {
13
+ throw new Error(
14
+ 'useLicenseContext must be used inside of the <Tldraw /> or <TldrawEditor /> components, or inside an <EditorProvider /> wrapping an editor created by them'
15
+ )
16
+ }
17
+ return licenseManager
18
+ }
19
+
20
+ /**
21
+ * Returns the license manager for the current editor, or `null` if there is none. Reads the
22
+ * license context when inside `<TldrawEditor />`, and otherwise falls back to the license manager
23
+ * of the nearest editor, so UI mounted outside the editor tree (via `EditorProvider`) resolves the
24
+ * same license as the editor it belongs to.
25
+ *
26
+ * @internal
27
+ */
28
+ export function useMaybeLicenseManager(): LicenseManager | null {
29
+ const licenseManager = useContext(LicenseContext)
30
+ const editor = useMaybeEditor()
31
+ return licenseManager ?? editor?.licenseManager ?? null
11
32
  }
12
33
 
13
34
  function shouldHideEditorAfterDelay(licenseState: string): boolean {
@@ -8,16 +8,18 @@ export function useLicenseManagerState(licenseManager: LicenseManager): LicenseS
8
8
 
9
9
  /**
10
10
  * Reactively reads whether a licensable feature is enabled for the current license. Re-renders when
11
- * license validation resolves.
11
+ * license validation resolves. Returns `false` when there is no license manager, so gated UI stays
12
+ * hidden when mounted without an editor or license context.
12
13
  *
13
14
  * @internal
14
15
  */
15
16
  export function useLicenseFeatureFlag(
16
- licenseManager: LicenseManager,
17
+ licenseManager: LicenseManager | null,
17
18
  feature: LicenseFeatureName
18
19
  ): boolean {
19
- return useValue('licenseFeature', () => licenseManager.isFeatureEnabled(feature), [
20
- licenseManager,
21
- feature,
22
- ])
20
+ return useValue(
21
+ 'licenseFeature',
22
+ () => (licenseManager ? licenseManager.isFeatureEnabled(feature) : false),
23
+ [licenseManager, feature]
24
+ )
23
25
  }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '5.3.0'
4
+ export const version = '5.3.2'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
7
  minor: '2026-08-05T11:39:54.978Z',
8
- patch: '2026-08-05T11:39:54.978Z',
8
+ patch: '2026-08-18T09:59:40.172Z',
9
9
  }