@tldraw/editor 3.16.0-canary.3be390323e1c → 3.16.0-canary.3e3b8b577bf1

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 (43) hide show
  1. package/dist-cjs/index.d.ts +15 -2
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/TldrawEditor.js +1 -1
  4. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  5. package/dist-cjs/lib/editor/Editor.js +9 -4
  6. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  7. package/dist-cjs/lib/license/LicenseManager.js +21 -4
  8. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  9. package/dist-cjs/lib/license/LicenseProvider.js +17 -1
  10. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  11. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +24 -2
  12. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  13. package/dist-cjs/lib/primitives/geometry/Group2d.js +5 -1
  14. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  15. package/dist-cjs/version.js +3 -3
  16. package/dist-cjs/version.js.map +1 -1
  17. package/dist-esm/index.d.mts +15 -2
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/lib/TldrawEditor.mjs +1 -1
  20. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  21. package/dist-esm/lib/editor/Editor.mjs +9 -4
  22. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  23. package/dist-esm/lib/license/LicenseManager.mjs +21 -4
  24. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  25. package/dist-esm/lib/license/LicenseProvider.mjs +16 -1
  26. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  27. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +24 -2
  28. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  29. package/dist-esm/lib/primitives/geometry/Group2d.mjs +5 -1
  30. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  31. package/dist-esm/version.mjs +3 -3
  32. package/dist-esm/version.mjs.map +1 -1
  33. package/package.json +7 -7
  34. package/src/lib/TldrawEditor.tsx +1 -2
  35. package/src/lib/editor/Editor.test.ts +90 -0
  36. package/src/lib/editor/Editor.ts +16 -4
  37. package/src/lib/license/LicenseManager.test.ts +78 -2
  38. package/src/lib/license/LicenseManager.ts +28 -5
  39. package/src/lib/license/LicenseProvider.tsx +40 -1
  40. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  41. package/src/lib/primitives/geometry/Geometry2d.ts +29 -2
  42. package/src/lib/primitives/geometry/Group2d.ts +6 -1
  43. package/src/version.ts +3 -3
@@ -833,3 +833,93 @@ describe('selectAll', () => {
833
833
  setSelectedShapesSpy.mockRestore()
834
834
  })
835
835
  })
836
+
837
+ describe('putExternalContent', () => {
838
+ let mockHandler: any
839
+
840
+ beforeEach(() => {
841
+ mockHandler = vi.fn()
842
+ editor.registerExternalContentHandler('text', mockHandler)
843
+ })
844
+
845
+ it('calls external content handler when not readonly', async () => {
846
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(false)
847
+
848
+ const info = { type: 'text' as const, text: 'test-data' }
849
+ await editor.putExternalContent(info)
850
+
851
+ expect(mockHandler).toHaveBeenCalledWith(info)
852
+ })
853
+
854
+ it('does not call external content handler when readonly', async () => {
855
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(true)
856
+
857
+ const info = { type: 'text' as const, text: 'test-data' }
858
+ await editor.putExternalContent(info)
859
+
860
+ expect(mockHandler).not.toHaveBeenCalled()
861
+ })
862
+
863
+ it('calls external content handler when readonly but force is true', async () => {
864
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(true)
865
+
866
+ const info = { type: 'text' as const, text: 'test-data' }
867
+ await editor.putExternalContent(info, { force: true })
868
+
869
+ expect(mockHandler).toHaveBeenCalledWith(info)
870
+ })
871
+
872
+ it('calls external content handler when force is false and not readonly', async () => {
873
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(false)
874
+
875
+ const info = { type: 'text' as const, text: 'test-data' }
876
+ await editor.putExternalContent(info, { force: false })
877
+
878
+ expect(mockHandler).toHaveBeenCalledWith(info)
879
+ })
880
+ })
881
+
882
+ describe('replaceExternalContent', () => {
883
+ let mockHandler: any
884
+
885
+ beforeEach(() => {
886
+ mockHandler = vi.fn()
887
+ editor.registerExternalContentHandler('text', mockHandler)
888
+ })
889
+
890
+ it('calls external content handler when not readonly', async () => {
891
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(false)
892
+
893
+ const info = { type: 'text' as const, text: 'test-data' }
894
+ await editor.replaceExternalContent(info)
895
+
896
+ expect(mockHandler).toHaveBeenCalledWith(info)
897
+ })
898
+
899
+ it('does not call external content handler when readonly', async () => {
900
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(true)
901
+
902
+ const info = { type: 'text' as const, text: 'test-data' }
903
+ await editor.replaceExternalContent(info)
904
+
905
+ expect(mockHandler).not.toHaveBeenCalled()
906
+ })
907
+
908
+ it('calls external content handler when readonly but force is true', async () => {
909
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(true)
910
+
911
+ const info = { type: 'text' as const, text: 'test-data' }
912
+ await editor.replaceExternalContent(info, { force: true })
913
+
914
+ expect(mockHandler).toHaveBeenCalledWith(info)
915
+ })
916
+
917
+ it('calls external content handler when force is false and not readonly', async () => {
918
+ vi.spyOn(editor, 'getIsReadonly').mockReturnValue(false)
919
+
920
+ const info = { type: 'text' as const, text: 'test-data' }
921
+ await editor.replaceExternalContent(info, { force: false })
922
+
923
+ expect(mockHandler).toHaveBeenCalledWith(info)
924
+ })
925
+ })
@@ -4680,8 +4680,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4680
4680
  return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
4681
4681
  const pageTransform = this.getShapePageTransform(shape)
4682
4682
  if (!pageTransform) return undefined
4683
- const geometry = this.getShapeGeometry(shape)
4684
- return Box.FromPoints(pageTransform.applyToPoints(geometry.vertices))
4683
+
4684
+ return Box.FromPoints(
4685
+ pageTransform.applyToPoints(this.getShapeGeometry(shape).boundsVertices)
4686
+ )
4685
4687
  })
4686
4688
  }
4687
4689
 
@@ -8831,8 +8833,13 @@ export class Editor extends EventEmitter<TLEventMap> {
8831
8833
  * Handle external content, such as files, urls, embeds, or plain text which has been put into the app, for example by pasting external text or dropping external images onto canvas.
8832
8834
  *
8833
8835
  * @param info - Info about the external content.
8836
+ * @param opts - Options for handling external content, including force flag to bypass readonly checks.
8834
8837
  */
8835
- async putExternalContent<E>(info: TLExternalContent<E>): Promise<void> {
8838
+ async putExternalContent<E>(
8839
+ info: TLExternalContent<E>,
8840
+ opts = {} as { force?: boolean }
8841
+ ): Promise<void> {
8842
+ if (!opts.force && this.getIsReadonly()) return
8836
8843
  return this.externalContentHandlers[info.type]?.(info as any)
8837
8844
  }
8838
8845
 
@@ -8840,8 +8847,13 @@ export class Editor extends EventEmitter<TLEventMap> {
8840
8847
  * Handle replacing external content.
8841
8848
  *
8842
8849
  * @param info - Info about the external content.
8850
+ * @param opts - Options for handling external content, including force flag to bypass readonly checks.
8843
8851
  */
8844
- async replaceExternalContent<E>(info: TLExternalContent<E>): Promise<void> {
8852
+ async replaceExternalContent<E>(
8853
+ info: TLExternalContent<E>,
8854
+ opts = {} as { force?: boolean }
8855
+ ): Promise<void> {
8856
+ if (!opts.force && this.getIsReadonly()) return
8845
8857
  return this.externalContentHandlers[info.type]?.(info as any)
8846
8858
  }
8847
8859
 
@@ -266,7 +266,7 @@ describe('LicenseManager', () => {
266
266
  delete window.location
267
267
  // @ts-ignore
268
268
  window.location = new URL(
269
- 'vscode-webview:vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app'
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
270
  )
271
271
 
272
272
  const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
@@ -286,7 +286,7 @@ describe('LicenseManager', () => {
286
286
  delete window.location
287
287
  // @ts-ignore
288
288
  window.location = new URL(
289
- 'vscode-webview:vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app'
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
290
  )
291
291
 
292
292
  const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
@@ -300,6 +300,70 @@ describe('LicenseManager', () => {
300
300
  )) as ValidLicenseKeyResult
301
301
  expect(result.isDomainValid).toBe(false)
302
302
  })
303
+
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
+ })
319
+
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
+ })
335
+
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
+ })
351
+
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
+ })
303
367
  })
304
368
 
305
369
  describe('License types and flags', () => {
@@ -316,6 +380,17 @@ describe('LicenseManager', () => {
316
380
  expect(result.isInternalLicense).toBe(true)
317
381
  })
318
382
 
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)
387
+
388
+ const result = (await licenseManager.getLicenseFromKey(
389
+ nativeLicenseKey
390
+ )) as ValidLicenseKeyResult
391
+ expect(result.isNativeLicense).toBe(true)
392
+ })
393
+
319
394
  it('Checks for license with watermark', async () => {
320
395
  const withWatermarkLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
321
396
  withWatermarkLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.WITH_WATERMARK
@@ -553,6 +628,7 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
553
628
  isAnnualLicense: true,
554
629
  isAnnualLicenseExpired: false,
555
630
  isInternalLicense: false,
631
+ isNativeLicense: false,
556
632
  isDevelopment: false,
557
633
  isDomainValid: true,
558
634
  isPerpetualLicense: false,
@@ -6,11 +6,22 @@ import { importPublicKey, str2ab } from '../utils/licensing'
6
6
  const GRACE_PERIOD_DAYS = 30
7
7
 
8
8
  export const FLAGS = {
9
+ // -- MUTUALLY EXCLUSIVE FLAGS --
10
+ // Annual means the license expires after a time period, usually 1 year.
9
11
  ANNUAL_LICENSE: 1,
12
+ // Perpetual means the license never expires up to the max supported version.
10
13
  PERPETUAL_LICENSE: 1 << 1,
14
+
15
+ // -- ADDITIVE FLAGS --
16
+ // Internal means the license is for internal use only.
11
17
  INTERNAL_LICENSE: 1 << 2,
18
+ // Watermark means the product is watermarked.
12
19
  WITH_WATERMARK: 1 << 3,
20
+ // Evaluation means the license is for evaluation purposes only.
13
21
  EVALUATION_LICENSE: 1 << 4,
22
+ // Native means the license is for native apps which switches
23
+ // on special-case logic.
24
+ NATIVE_LICENSE: 1 << 5,
14
25
  }
15
26
  const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
16
27
 
@@ -69,6 +80,7 @@ export interface ValidLicenseKeyResult {
69
80
  isPerpetualLicense: boolean
70
81
  isPerpetualLicenseExpired: boolean
71
82
  isInternalLicense: boolean
83
+ isNativeLicense: boolean
72
84
  isLicensedWithWatermark: boolean
73
85
  isEvaluationLicense: boolean
74
86
  isEvaluationLicenseExpired: boolean
@@ -271,6 +283,7 @@ export class LicenseManager {
271
283
  isPerpetualLicense,
272
284
  isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
273
285
  isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
286
+ isNativeLicense: this.isNativeLicense(licenseInfo),
274
287
  isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
275
288
  isEvaluationLicense,
276
289
  isEvaluationLicenseExpired:
@@ -291,13 +304,13 @@ export class LicenseManager {
291
304
  const currentHostname = window.location.hostname.toLowerCase()
292
305
 
293
306
  return licenseInfo.hosts.some((host) => {
294
- const normalizedHost = host.toLowerCase().trim()
307
+ const normalizedHostOrUrlRegex = host.toLowerCase().trim()
295
308
 
296
309
  // Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'
297
310
  if (
298
- normalizedHost === currentHostname ||
299
- `www.${normalizedHost}` === currentHostname ||
300
- normalizedHost === `www.${currentHostname}`
311
+ normalizedHostOrUrlRegex === currentHostname ||
312
+ `www.${normalizedHostOrUrlRegex}` === currentHostname ||
313
+ normalizedHostOrUrlRegex === `www.${currentHostname}`
301
314
  ) {
302
315
  return true
303
316
  }
@@ -308,6 +321,12 @@ export class LicenseManager {
308
321
  return true
309
322
  }
310
323
 
324
+ // Native license support
325
+ // In this case, `normalizedHost` is actually a protocol, e.g. `app-bundle:`
326
+ if (this.isNativeLicense(licenseInfo)) {
327
+ return new RegExp(normalizedHostOrUrlRegex).test(window.location.href)
328
+ }
329
+
311
330
  // Glob testing, we only support '*.somedomain.com' right now.
312
331
  if (host.includes('*')) {
313
332
  const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
@@ -318,7 +337,7 @@ export class LicenseManager {
318
337
  if (window.location.protocol === 'vscode-webview:') {
319
338
  const currentUrl = new URL(window.location.href)
320
339
  const extensionId = currentUrl.searchParams.get('extensionId')
321
- if (normalizedHost === extensionId) {
340
+ if (normalizedHostOrUrlRegex === extensionId) {
322
341
  return true
323
342
  }
324
343
  }
@@ -327,6 +346,10 @@ export class LicenseManager {
327
346
  })
328
347
  }
329
348
 
349
+ private isNativeLicense(licenseInfo: LicenseInfo) {
350
+ return this.isFlagEnabled(licenseInfo.flags, FLAGS.NATIVE_LICENSE)
351
+ }
352
+
330
353
  private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
331
354
  return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
332
355
  }
@@ -17,7 +17,7 @@ export const LICENSE_TIMEOUT = 5000
17
17
 
18
18
  /** @internal */
19
19
  export function LicenseProvider({
20
- licenseKey,
20
+ licenseKey = getLicenseKeyFromEnv() ?? undefined,
21
21
  children,
22
22
  }: {
23
23
  licenseKey?: string
@@ -51,3 +51,42 @@ export function LicenseProvider({
51
51
  function LicenseGate() {
52
52
  return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
53
53
  }
54
+
55
+ let envLicenseKey: string | undefined | null = undefined
56
+ function getLicenseKeyFromEnv() {
57
+ if (envLicenseKey !== undefined) {
58
+ return envLicenseKey
59
+ }
60
+ // it's important here that we write out the full process.env.WHATEVER expression instead of
61
+ // doing something like process.env[someVariable]. This is because most bundlers do something
62
+ // like a find-replace inject environment variables, and so won't pick up on dynamic ones. It
63
+ // also means we can't do checks like `process.env && process.env.WHATEVER`, which is why we use
64
+ // the `getEnv` try/catch approach.
65
+
66
+ // framework-specific prefixes borrowed from the ones vercel uses, but trimmed down to just the
67
+ // react-y ones: https://vercel.com/docs/environment-variables/framework-environment-variables
68
+ envLicenseKey =
69
+ getEnv(() => process.env.TLDRAW_LICENSE_KEY) ||
70
+ getEnv(() => process.env.NEXT_PUBLIC_TLDRAW_LICENSE_KEY) ||
71
+ getEnv(() => process.env.REACT_APP_TLDRAW_LICENSE_KEY) ||
72
+ getEnv(() => process.env.GATSBY_TLDRAW_LICENSE_KEY) ||
73
+ getEnv(() => process.env.VITE_TLDRAW_LICENSE_KEY) ||
74
+ getEnv(() => process.env.PUBLIC_TLDRAW_LICENSE_KEY) ||
75
+ getEnv(() => (import.meta as any).env.TLDRAW_LICENSE_KEY) ||
76
+ getEnv(() => (import.meta as any).env.NEXT_PUBLIC_TLDRAW_LICENSE_KEY) ||
77
+ getEnv(() => (import.meta as any).env.REACT_APP_TLDRAW_LICENSE_KEY) ||
78
+ getEnv(() => (import.meta as any).env.GATSBY_TLDRAW_LICENSE_KEY) ||
79
+ getEnv(() => (import.meta as any).env.VITE_TLDRAW_LICENSE_KEY) ||
80
+ getEnv(() => (import.meta as any).env.PUBLIC_TLDRAW_LICENSE_KEY) ||
81
+ null
82
+
83
+ return envLicenseKey
84
+ }
85
+
86
+ function getEnv(cb: () => string | undefined) {
87
+ try {
88
+ return cb()
89
+ } catch {
90
+ return undefined
91
+ }
92
+ }