@tldraw/editor 3.13.0-canary.52e2314962c1 → 3.13.0-canary.9061a9bbd3fb
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.
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +6 -6
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +25 -20
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/license/LicenseManager.js +8 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +1 -0
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +6 -6
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +25 -20
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/license/LicenseManager.mjs +8 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +6 -0
- package/package.json +7 -7
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +10 -6
- package/src/lib/editor/Editor.ts +22 -19
- package/src/lib/license/LicenseManager.test.ts +40 -0
- package/src/lib/license/LicenseManager.ts +13 -1
- package/src/version.ts +3 -3
|
@@ -52,7 +52,7 @@ class LicenseManager {
|
|
|
52
52
|
getIsDevelopment(testEnvironment) {
|
|
53
53
|
if (testEnvironment === "development") return true;
|
|
54
54
|
if (testEnvironment === "production") return false;
|
|
55
|
-
return window.location.protocol
|
|
55
|
+
return !["https:", "vscode-webview:"].includes(window.location.protocol) || window.location.hostname === "localhost";
|
|
56
56
|
}
|
|
57
57
|
async extractLicenseKey(licenseKey) {
|
|
58
58
|
const [data, signature] = licenseKey.split(".");
|
|
@@ -155,6 +155,13 @@ class LicenseManager {
|
|
|
155
155
|
const globToRegex = new RegExp(host.replace(/\*/g, ".*?"));
|
|
156
156
|
return globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`);
|
|
157
157
|
}
|
|
158
|
+
if (window.location.protocol === "vscode-webview:") {
|
|
159
|
+
const currentUrl = new URL(window.location.href);
|
|
160
|
+
const extensionId = currentUrl.searchParams.get("extensionId");
|
|
161
|
+
if (normalizedHost === extensionId) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
158
165
|
return false;
|
|
159
166
|
});
|
|
160
167
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/license/LicenseManager.ts"],
|
|
4
|
-
"sourcesContent": ["import { atom } from '@tldraw/state'\nimport { fetch } from '@tldraw/utils'\nimport { publishDates } from '../../version'\nimport { getDefaultCdnBaseUrl } from '../utils/assets'\nimport { importPublicKey, str2ab } from '../utils/licensing'\n\nconst GRACE_PERIOD_DAYS = 5\n\nexport const FLAGS = {\n\tANNUAL_LICENSE: 0x1,\n\tPERPETUAL_LICENSE: 0x2,\n\tINTERNAL_LICENSE: 0x4,\n\tWITH_WATERMARK: 0x8,\n}\nconst HIGHEST_FLAG = Math.max(...Object.values(FLAGS))\n\nexport const PROPERTIES = {\n\tID: 0,\n\tHOSTS: 1,\n\tFLAGS: 2,\n\tEXPIRY_DATE: 3,\n}\nconst NUMBER_OF_KNOWN_PROPERTIES = Object.keys(PROPERTIES).length\n\nconst LICENSE_EMAIL = 'sales@tldraw.com'\n\nconst WATERMARK_TRACK_SRC = `${getDefaultCdnBaseUrl()}/watermarks/watermark-track.svg`\n\n/** @internal */\nexport interface LicenseInfo {\n\tid: string\n\thosts: string[]\n\tflags: number\n\texpiryDate: string\n}\n/** @internal */\nexport type InvalidLicenseReason =\n\t| 'invalid-license-key'\n\t| 'no-key-provided'\n\t| 'has-key-development-mode'\n\n/** @internal */\nexport type LicenseFromKeyResult = InvalidLicenseKeyResult | ValidLicenseKeyResult\n\n/** @internal */\nexport interface InvalidLicenseKeyResult {\n\tisLicenseParseable: false\n\treason: InvalidLicenseReason\n}\n\n/** @internal */\nexport interface ValidLicenseKeyResult {\n\tisLicenseParseable: true\n\tlicense: LicenseInfo\n\tisDevelopment: boolean\n\tisDomainValid: boolean\n\texpiryDate: Date\n\tisAnnualLicense: boolean\n\tisAnnualLicenseExpired: boolean\n\tisPerpetualLicense: boolean\n\tisPerpetualLicenseExpired: boolean\n\tisInternalLicense: boolean\n\tisLicensedWithWatermark: boolean\n}\n\n/** @internal */\nexport type TestEnvironment = 'development' | 'production'\n\n/** @internal */\nexport class LicenseManager {\n\tprivate publicKey =\n\t\t'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHJh0uUfxHtCGyerXmmatE368Hd9rI6LH9oPDQihnaCryRFWEVeOvf9U/SPbyxX74LFyJs5tYeAHq5Nc0Ax25LQ'\n\tpublic isDevelopment: boolean\n\tpublic isTest: boolean\n\tpublic isCryptoAvailable: boolean\n\tstate = atom<'pending' | 'licensed' | 'licensed-with-watermark' | 'unlicensed'>(\n\t\t'license state',\n\t\t'pending'\n\t)\n\tpublic verbose = true\n\n\tconstructor(\n\t\tlicenseKey: string | undefined,\n\t\ttestPublicKey?: string,\n\t\ttestEnvironment?: TestEnvironment\n\t) {\n\t\tthis.isTest = process.env.NODE_ENV === 'test'\n\t\tthis.isDevelopment = this.getIsDevelopment(testEnvironment)\n\t\tthis.publicKey = testPublicKey || this.publicKey\n\t\tthis.isCryptoAvailable = !!crypto.subtle\n\n\t\tthis.getLicenseFromKey(licenseKey).then((result) => {\n\t\t\tconst isUnlicensed = isEditorUnlicensed(result)\n\n\t\t\tif (!this.isDevelopment && isUnlicensed) {\n\t\t\t\tfetch(WATERMARK_TRACK_SRC)\n\t\t\t}\n\n\t\t\tif (isUnlicensed) {\n\t\t\t\tthis.state.set('unlicensed')\n\t\t\t} else if ((result as ValidLicenseKeyResult).isLicensedWithWatermark) {\n\t\t\t\tthis.state.set('licensed-with-watermark')\n\t\t\t} else {\n\t\t\t\tthis.state.set('licensed')\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate getIsDevelopment(testEnvironment?: TestEnvironment) {\n\t\tif (testEnvironment === 'development') return true\n\t\tif (testEnvironment === 'production') return false\n\n\t\t// If we are using https on a non-localhost domain we assume it's a production env and a development one otherwise\n\t\treturn window.location.protocol !== 'https:' || window.location.hostname === 'localhost'\n\t}\n\n\tprivate async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {\n\t\tconst [data, signature] = licenseKey.split('.')\n\t\tconst [prefix, encodedData] = data.split('/')\n\n\t\tif (!prefix.startsWith('tldraw-')) {\n\t\t\tthrow new Error(`Unsupported prefix '${prefix}'`)\n\t\t}\n\n\t\tconst publicCryptoKey = await importPublicKey(this.publicKey)\n\n\t\tlet isVerified\n\t\ttry {\n\t\t\tisVerified = await crypto.subtle.verify(\n\t\t\t\t{\n\t\t\t\t\tname: 'ECDSA',\n\t\t\t\t\thash: { name: 'SHA-256' },\n\t\t\t\t},\n\t\t\t\tpublicCryptoKey,\n\t\t\t\tnew Uint8Array(str2ab(atob(signature))),\n\t\t\t\tnew Uint8Array(str2ab(atob(encodedData)))\n\t\t\t)\n\t\t} catch (e) {\n\t\t\tconsole.error(e)\n\t\t\tthrow new Error('Could not perform signature validation')\n\t\t}\n\n\t\tif (!isVerified) {\n\t\t\tthrow new Error('Invalid signature')\n\t\t}\n\n\t\tlet decodedData: any\n\t\ttry {\n\t\t\tdecodedData = JSON.parse(atob(encodedData))\n\t\t} catch {\n\t\t\tthrow new Error('Could not parse object')\n\t\t}\n\t\tif (decodedData.length > NUMBER_OF_KNOWN_PROPERTIES) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'License key contains some unknown properties.',\n\t\t\t\t'You may want to update tldraw packages to a newer version to get access to new functionality.',\n\t\t\t])\n\t\t}\n\n\t\treturn {\n\t\t\tid: decodedData[PROPERTIES.ID],\n\t\t\thosts: decodedData[PROPERTIES.HOSTS],\n\t\t\tflags: decodedData[PROPERTIES.FLAGS],\n\t\t\texpiryDate: decodedData[PROPERTIES.EXPIRY_DATE],\n\t\t}\n\t}\n\n\tasync getLicenseFromKey(licenseKey?: string): Promise<LicenseFromKeyResult> {\n\t\tif (!licenseKey) {\n\t\t\tif (!this.isDevelopment) {\n\t\t\t\tthis.outputNoLicenseKeyProvided()\n\t\t\t}\n\n\t\t\treturn { isLicenseParseable: false, reason: 'no-key-provided' }\n\t\t}\n\n\t\tif (this.isDevelopment && !this.isCryptoAvailable) {\n\t\t\tif (this.verbose) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.log(\n\t\t\t\t\t'tldraw: you seem to be in a development environment that does not support crypto. License not verified.'\n\t\t\t\t)\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.log('You should check that this works in production separately.')\n\t\t\t}\n\t\t\t// We can't parse the license if we are in development mode since crypto\n\t\t\t// is not available on http\n\t\t\treturn { isLicenseParseable: false, reason: 'has-key-development-mode' }\n\t\t}\n\n\t\t// Borrowed idea from AG Grid:\n\t\t// Copying from various sources (like PDFs) can include zero-width characters.\n\t\t// This helps makes sure the key validation doesn't fail.\n\t\tlet cleanedLicenseKey = licenseKey.replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n\t\tcleanedLicenseKey = cleanedLicenseKey.replace(/\\r?\\n|\\r/g, '')\n\n\t\ttry {\n\t\t\tconst licenseInfo = await this.extractLicenseKey(cleanedLicenseKey)\n\t\t\tconst expiryDate = new Date(licenseInfo.expiryDate)\n\t\t\tconst isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)\n\t\t\tconst isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)\n\n\t\t\tconst result: ValidLicenseKeyResult = {\n\t\t\t\tlicense: licenseInfo,\n\t\t\t\tisLicenseParseable: true,\n\t\t\t\tisDevelopment: this.isDevelopment,\n\t\t\t\tisDomainValid: this.isDomainValid(licenseInfo),\n\t\t\t\texpiryDate,\n\t\t\t\tisAnnualLicense,\n\t\t\t\tisAnnualLicenseExpired: isAnnualLicense && this.isAnnualLicenseExpired(expiryDate),\n\t\t\t\tisPerpetualLicense,\n\t\t\t\tisPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),\n\t\t\t\tisInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),\n\t\t\t\tisLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),\n\t\t\t}\n\t\t\tthis.outputLicenseInfoIfNeeded(result)\n\n\t\t\treturn result\n\t\t} catch (e: any) {\n\t\t\tthis.outputInvalidLicenseKey(e.message)\n\t\t\t// If the license can't be parsed, it's invalid\n\t\t\treturn { isLicenseParseable: false, reason: 'invalid-license-key' }\n\t\t}\n\t}\n\n\tprivate isDomainValid(licenseInfo: LicenseInfo) {\n\t\tconst currentHostname = window.location.hostname.toLowerCase()\n\n\t\treturn licenseInfo.hosts.some((host) => {\n\t\t\tconst normalizedHost = host.toLowerCase().trim()\n\n\t\t\t// Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'\n\t\t\tif (\n\t\t\t\tnormalizedHost === currentHostname ||\n\t\t\t\t`www.${normalizedHost}` === currentHostname ||\n\t\t\t\tnormalizedHost === `www.${currentHostname}`\n\t\t\t) {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// If host is '*', we allow all domains.\n\t\t\tif (host === '*') {\n\t\t\t\t// All domains allowed.\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// Glob testing, we only support '*.somedomain.com' right now.\n\t\t\tif (host.includes('*')) {\n\t\t\t\tconst globToRegex = new RegExp(host.replace(/\\*/g, '.*?'))\n\t\t\t\treturn globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`)\n\t\t\t}\n\n\t\t\treturn false\n\t\t})\n\t}\n\n\tprivate getExpirationDateWithoutGracePeriod(expiryDate: Date) {\n\t\treturn new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())\n\t}\n\n\tprivate getExpirationDateWithGracePeriod(expiryDate: Date) {\n\t\treturn new Date(\n\t\t\texpiryDate.getFullYear(),\n\t\t\texpiryDate.getMonth(),\n\t\t\texpiryDate.getDate() + GRACE_PERIOD_DAYS + 1 // Add 1 day to include the expiration day\n\t\t)\n\t}\n\n\tprivate isAnnualLicenseExpired(expiryDate: Date) {\n\t\tconst expiration = this.getExpirationDateWithGracePeriod(expiryDate)\n\t\tconst isExpired = new Date() >= expiration\n\t\t// If it is not expired yet (including the grace period), but after the expiry date we warn the users\n\t\tif (!isExpired && new Date() >= this.getExpirationDateWithoutGracePeriod(expiryDate)) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'tldraw license is about to expire, you are in a grace period.',\n\t\t\t\t`Please reach out to ${LICENSE_EMAIL} if you would like to renew your license.`,\n\t\t\t])\n\t\t}\n\t\treturn isExpired\n\t}\n\n\tprivate isPerpetualLicenseExpired(expiryDate: Date) {\n\t\tconst expiration = this.getExpirationDateWithGracePeriod(expiryDate)\n\t\tconst dates = {\n\t\t\tmajor: new Date(publishDates.major),\n\t\t\tminor: new Date(publishDates.minor),\n\t\t}\n\t\t// We allow patch releases, but the major and minor releases should be within the expiration date\n\t\treturn dates.major >= expiration || dates.minor >= expiration\n\t}\n\n\tprivate isFlagEnabled(flags: number, flag: number) {\n\t\treturn (flags & flag) === flag\n\t}\n\n\tprivate outputNoLicenseKeyProvided() {\n\t\t// Noop, we don't need to show this message.\n\t\t// this.outputMessages([\n\t\t// \t'No tldraw license key provided!',\n\t\t// \t`Please reach out to ${LICENSE_EMAIL} if you would like to license tldraw or if you'd like a trial.`,\n\t\t// ])\n\t}\n\n\tprivate outputInvalidLicenseKey(msg: string) {\n\t\tthis.outputMessages(['Invalid tldraw license key', `Reason: ${msg}`])\n\t}\n\n\tprivate outputLicenseInfoIfNeeded(result: ValidLicenseKeyResult) {\n\t\tif (result.isAnnualLicenseExpired) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'Your tldraw license has expired!',\n\t\t\t\t`Please reach out to ${LICENSE_EMAIL} to renew.`,\n\t\t\t])\n\t\t}\n\n\t\tif (!result.isDomainValid && !result.isDevelopment) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'This tldraw license key is not valid for this domain!',\n\t\t\t\t`Please reach out to ${LICENSE_EMAIL} if you would like to use tldraw on other domains.`,\n\t\t\t])\n\t\t}\n\t\t// If we added a new flag it will be twice the value of the currently highest flag.\n\t\t// And if all the current flags are on we would get the `HIGHEST_FLAG * 2 - 1`, so anything higher than that means there are new flags.\n\t\tif (result.license.flags >= HIGHEST_FLAG * 2) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'This tldraw license contains some unknown flags.',\n\t\t\t\t'You may want to update tldraw packages to a newer version to get access to new functionality.',\n\t\t\t])\n\t\t}\n\t}\n\n\tprivate outputMessages(messages: string[]) {\n\t\tif (this.isTest) return\n\t\tif (this.verbose) {\n\t\t\tthis.outputDelimiter()\n\t\t\tfor (const message of messages) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.log(\n\t\t\t\t\t`%c${message}`,\n\t\t\t\t\t`color: white; background: crimson; padding: 2px; border-radius: 3px;`\n\t\t\t\t)\n\t\t\t}\n\t\t\tthis.outputDelimiter()\n\t\t}\n\t}\n\n\tprivate outputDelimiter() {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.log(\n\t\t\t'%c-------------------------------------------------------------------',\n\t\t\t`color: white; background: crimson; padding: 2px; border-radius: 3px;`\n\t\t)\n\t}\n\n\tstatic className = 'tl-watermark_SEE-LICENSE'\n}\n\nexport function isEditorUnlicensed(result: LicenseFromKeyResult) {\n\tif (!result.isLicenseParseable) return true\n\tif (!result.isDomainValid && !result.isDevelopment) return true\n\tif (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {\n\t\tif (result.isInternalLicense) {\n\t\t\tthrow new Error('License: Internal license expired.')\n\t\t}\n\t\treturn true\n\t}\n\n\treturn false\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,iBAAiB,cAAc;AAExC,MAAM,oBAAoB;AAEnB,MAAM,QAAQ;AAAA,EACpB,gBAAgB;AAAA,EAChB,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,gBAAgB;AACjB;AACA,MAAM,eAAe,KAAK,IAAI,GAAG,OAAO,OAAO,KAAK,CAAC;AAE9C,MAAM,aAAa;AAAA,EACzB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,OAAO;AAAA,EACP,aAAa;AACd;AACA,MAAM,6BAA6B,OAAO,KAAK,UAAU,EAAE;AAE3D,MAAM,gBAAgB;AAEtB,MAAM,sBAAsB,GAAG,qBAAqB,CAAC;AA2C9C,MAAM,eAAe;AAAA,EACnB,YACP;AAAA,EACM;AAAA,EACA;AAAA,EACA;AAAA,EACP,QAAQ;AAAA,IACP;AAAA,IACA;AAAA,EACD;AAAA,EACO,UAAU;AAAA,EAEjB,YACC,YACA,eACA,iBACC;AACD,SAAK,SAAS,QAAQ,IAAI,aAAa;AACvC,SAAK,gBAAgB,KAAK,iBAAiB,eAAe;AAC1D,SAAK,YAAY,iBAAiB,KAAK;AACvC,SAAK,oBAAoB,CAAC,CAAC,OAAO;AAElC,SAAK,kBAAkB,UAAU,EAAE,KAAK,CAAC,WAAW;AACnD,YAAM,eAAe,mBAAmB,MAAM;AAE9C,UAAI,CAAC,KAAK,iBAAiB,cAAc;AACxC,cAAM,mBAAmB;AAAA,MAC1B;AAEA,UAAI,cAAc;AACjB,aAAK,MAAM,IAAI,YAAY;AAAA,MAC5B,WAAY,OAAiC,yBAAyB;AACrE,aAAK,MAAM,IAAI,yBAAyB;AAAA,MACzC,OAAO;AACN,aAAK,MAAM,IAAI,UAAU;AAAA,MAC1B;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,iBAAiB,iBAAmC;AAC3D,QAAI,oBAAoB,cAAe,QAAO;AAC9C,QAAI,oBAAoB,aAAc,QAAO;AAG7C,
|
|
4
|
+
"sourcesContent": ["import { atom } from '@tldraw/state'\nimport { fetch } from '@tldraw/utils'\nimport { publishDates } from '../../version'\nimport { getDefaultCdnBaseUrl } from '../utils/assets'\nimport { importPublicKey, str2ab } from '../utils/licensing'\n\nconst GRACE_PERIOD_DAYS = 5\n\nexport const FLAGS = {\n\tANNUAL_LICENSE: 0x1,\n\tPERPETUAL_LICENSE: 0x2,\n\tINTERNAL_LICENSE: 0x4,\n\tWITH_WATERMARK: 0x8,\n}\nconst HIGHEST_FLAG = Math.max(...Object.values(FLAGS))\n\nexport const PROPERTIES = {\n\tID: 0,\n\tHOSTS: 1,\n\tFLAGS: 2,\n\tEXPIRY_DATE: 3,\n}\nconst NUMBER_OF_KNOWN_PROPERTIES = Object.keys(PROPERTIES).length\n\nconst LICENSE_EMAIL = 'sales@tldraw.com'\n\nconst WATERMARK_TRACK_SRC = `${getDefaultCdnBaseUrl()}/watermarks/watermark-track.svg`\n\n/** @internal */\nexport interface LicenseInfo {\n\tid: string\n\thosts: string[]\n\tflags: number\n\texpiryDate: string\n}\n/** @internal */\nexport type InvalidLicenseReason =\n\t| 'invalid-license-key'\n\t| 'no-key-provided'\n\t| 'has-key-development-mode'\n\n/** @internal */\nexport type LicenseFromKeyResult = InvalidLicenseKeyResult | ValidLicenseKeyResult\n\n/** @internal */\nexport interface InvalidLicenseKeyResult {\n\tisLicenseParseable: false\n\treason: InvalidLicenseReason\n}\n\n/** @internal */\nexport interface ValidLicenseKeyResult {\n\tisLicenseParseable: true\n\tlicense: LicenseInfo\n\tisDevelopment: boolean\n\tisDomainValid: boolean\n\texpiryDate: Date\n\tisAnnualLicense: boolean\n\tisAnnualLicenseExpired: boolean\n\tisPerpetualLicense: boolean\n\tisPerpetualLicenseExpired: boolean\n\tisInternalLicense: boolean\n\tisLicensedWithWatermark: boolean\n}\n\n/** @internal */\nexport type TestEnvironment = 'development' | 'production'\n\n/** @internal */\nexport class LicenseManager {\n\tprivate publicKey =\n\t\t'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHJh0uUfxHtCGyerXmmatE368Hd9rI6LH9oPDQihnaCryRFWEVeOvf9U/SPbyxX74LFyJs5tYeAHq5Nc0Ax25LQ'\n\tpublic isDevelopment: boolean\n\tpublic isTest: boolean\n\tpublic isCryptoAvailable: boolean\n\tstate = atom<'pending' | 'licensed' | 'licensed-with-watermark' | 'unlicensed'>(\n\t\t'license state',\n\t\t'pending'\n\t)\n\tpublic verbose = true\n\n\tconstructor(\n\t\tlicenseKey: string | undefined,\n\t\ttestPublicKey?: string,\n\t\ttestEnvironment?: TestEnvironment\n\t) {\n\t\tthis.isTest = process.env.NODE_ENV === 'test'\n\t\tthis.isDevelopment = this.getIsDevelopment(testEnvironment)\n\t\tthis.publicKey = testPublicKey || this.publicKey\n\t\tthis.isCryptoAvailable = !!crypto.subtle\n\n\t\tthis.getLicenseFromKey(licenseKey).then((result) => {\n\t\t\tconst isUnlicensed = isEditorUnlicensed(result)\n\n\t\t\tif (!this.isDevelopment && isUnlicensed) {\n\t\t\t\tfetch(WATERMARK_TRACK_SRC)\n\t\t\t}\n\n\t\t\tif (isUnlicensed) {\n\t\t\t\tthis.state.set('unlicensed')\n\t\t\t} else if ((result as ValidLicenseKeyResult).isLicensedWithWatermark) {\n\t\t\t\tthis.state.set('licensed-with-watermark')\n\t\t\t} else {\n\t\t\t\tthis.state.set('licensed')\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate getIsDevelopment(testEnvironment?: TestEnvironment) {\n\t\tif (testEnvironment === 'development') return true\n\t\tif (testEnvironment === 'production') return false\n\n\t\t// If we are using https on a non-localhost domain we assume it's a production env and a development one otherwise\n\t\treturn (\n\t\t\t!['https:', 'vscode-webview:'].includes(window.location.protocol) ||\n\t\t\twindow.location.hostname === 'localhost'\n\t\t)\n\t}\n\n\tprivate async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {\n\t\tconst [data, signature] = licenseKey.split('.')\n\t\tconst [prefix, encodedData] = data.split('/')\n\n\t\tif (!prefix.startsWith('tldraw-')) {\n\t\t\tthrow new Error(`Unsupported prefix '${prefix}'`)\n\t\t}\n\n\t\tconst publicCryptoKey = await importPublicKey(this.publicKey)\n\n\t\tlet isVerified\n\t\ttry {\n\t\t\tisVerified = await crypto.subtle.verify(\n\t\t\t\t{\n\t\t\t\t\tname: 'ECDSA',\n\t\t\t\t\thash: { name: 'SHA-256' },\n\t\t\t\t},\n\t\t\t\tpublicCryptoKey,\n\t\t\t\tnew Uint8Array(str2ab(atob(signature))),\n\t\t\t\tnew Uint8Array(str2ab(atob(encodedData)))\n\t\t\t)\n\t\t} catch (e) {\n\t\t\tconsole.error(e)\n\t\t\tthrow new Error('Could not perform signature validation')\n\t\t}\n\n\t\tif (!isVerified) {\n\t\t\tthrow new Error('Invalid signature')\n\t\t}\n\n\t\tlet decodedData: any\n\t\ttry {\n\t\t\tdecodedData = JSON.parse(atob(encodedData))\n\t\t} catch {\n\t\t\tthrow new Error('Could not parse object')\n\t\t}\n\t\tif (decodedData.length > NUMBER_OF_KNOWN_PROPERTIES) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'License key contains some unknown properties.',\n\t\t\t\t'You may want to update tldraw packages to a newer version to get access to new functionality.',\n\t\t\t])\n\t\t}\n\n\t\treturn {\n\t\t\tid: decodedData[PROPERTIES.ID],\n\t\t\thosts: decodedData[PROPERTIES.HOSTS],\n\t\t\tflags: decodedData[PROPERTIES.FLAGS],\n\t\t\texpiryDate: decodedData[PROPERTIES.EXPIRY_DATE],\n\t\t}\n\t}\n\n\tasync getLicenseFromKey(licenseKey?: string): Promise<LicenseFromKeyResult> {\n\t\tif (!licenseKey) {\n\t\t\tif (!this.isDevelopment) {\n\t\t\t\tthis.outputNoLicenseKeyProvided()\n\t\t\t}\n\n\t\t\treturn { isLicenseParseable: false, reason: 'no-key-provided' }\n\t\t}\n\n\t\tif (this.isDevelopment && !this.isCryptoAvailable) {\n\t\t\tif (this.verbose) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.log(\n\t\t\t\t\t'tldraw: you seem to be in a development environment that does not support crypto. License not verified.'\n\t\t\t\t)\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.log('You should check that this works in production separately.')\n\t\t\t}\n\t\t\t// We can't parse the license if we are in development mode since crypto\n\t\t\t// is not available on http\n\t\t\treturn { isLicenseParseable: false, reason: 'has-key-development-mode' }\n\t\t}\n\n\t\t// Borrowed idea from AG Grid:\n\t\t// Copying from various sources (like PDFs) can include zero-width characters.\n\t\t// This helps makes sure the key validation doesn't fail.\n\t\tlet cleanedLicenseKey = licenseKey.replace(/[\\u200B-\\u200D\\uFEFF]/g, '')\n\t\tcleanedLicenseKey = cleanedLicenseKey.replace(/\\r?\\n|\\r/g, '')\n\n\t\ttry {\n\t\t\tconst licenseInfo = await this.extractLicenseKey(cleanedLicenseKey)\n\t\t\tconst expiryDate = new Date(licenseInfo.expiryDate)\n\t\t\tconst isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)\n\t\t\tconst isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)\n\n\t\t\tconst result: ValidLicenseKeyResult = {\n\t\t\t\tlicense: licenseInfo,\n\t\t\t\tisLicenseParseable: true,\n\t\t\t\tisDevelopment: this.isDevelopment,\n\t\t\t\tisDomainValid: this.isDomainValid(licenseInfo),\n\t\t\t\texpiryDate,\n\t\t\t\tisAnnualLicense,\n\t\t\t\tisAnnualLicenseExpired: isAnnualLicense && this.isAnnualLicenseExpired(expiryDate),\n\t\t\t\tisPerpetualLicense,\n\t\t\t\tisPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),\n\t\t\t\tisInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),\n\t\t\t\tisLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),\n\t\t\t}\n\t\t\tthis.outputLicenseInfoIfNeeded(result)\n\n\t\t\treturn result\n\t\t} catch (e: any) {\n\t\t\tthis.outputInvalidLicenseKey(e.message)\n\t\t\t// If the license can't be parsed, it's invalid\n\t\t\treturn { isLicenseParseable: false, reason: 'invalid-license-key' }\n\t\t}\n\t}\n\n\tprivate isDomainValid(licenseInfo: LicenseInfo) {\n\t\tconst currentHostname = window.location.hostname.toLowerCase()\n\n\t\treturn licenseInfo.hosts.some((host) => {\n\t\t\tconst normalizedHost = host.toLowerCase().trim()\n\n\t\t\t// Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'\n\t\t\tif (\n\t\t\t\tnormalizedHost === currentHostname ||\n\t\t\t\t`www.${normalizedHost}` === currentHostname ||\n\t\t\t\tnormalizedHost === `www.${currentHostname}`\n\t\t\t) {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// If host is '*', we allow all domains.\n\t\t\tif (host === '*') {\n\t\t\t\t// All domains allowed.\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// Glob testing, we only support '*.somedomain.com' right now.\n\t\t\tif (host.includes('*')) {\n\t\t\t\tconst globToRegex = new RegExp(host.replace(/\\*/g, '.*?'))\n\t\t\t\treturn globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`)\n\t\t\t}\n\n\t\t\t// VSCode support\n\t\t\tif (window.location.protocol === 'vscode-webview:') {\n\t\t\t\tconst currentUrl = new URL(window.location.href)\n\t\t\t\tconst extensionId = currentUrl.searchParams.get('extensionId')\n\t\t\t\tif (normalizedHost === extensionId) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false\n\t\t})\n\t}\n\n\tprivate getExpirationDateWithoutGracePeriod(expiryDate: Date) {\n\t\treturn new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())\n\t}\n\n\tprivate getExpirationDateWithGracePeriod(expiryDate: Date) {\n\t\treturn new Date(\n\t\t\texpiryDate.getFullYear(),\n\t\t\texpiryDate.getMonth(),\n\t\t\texpiryDate.getDate() + GRACE_PERIOD_DAYS + 1 // Add 1 day to include the expiration day\n\t\t)\n\t}\n\n\tprivate isAnnualLicenseExpired(expiryDate: Date) {\n\t\tconst expiration = this.getExpirationDateWithGracePeriod(expiryDate)\n\t\tconst isExpired = new Date() >= expiration\n\t\t// If it is not expired yet (including the grace period), but after the expiry date we warn the users\n\t\tif (!isExpired && new Date() >= this.getExpirationDateWithoutGracePeriod(expiryDate)) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'tldraw license is about to expire, you are in a grace period.',\n\t\t\t\t`Please reach out to ${LICENSE_EMAIL} if you would like to renew your license.`,\n\t\t\t])\n\t\t}\n\t\treturn isExpired\n\t}\n\n\tprivate isPerpetualLicenseExpired(expiryDate: Date) {\n\t\tconst expiration = this.getExpirationDateWithGracePeriod(expiryDate)\n\t\tconst dates = {\n\t\t\tmajor: new Date(publishDates.major),\n\t\t\tminor: new Date(publishDates.minor),\n\t\t}\n\t\t// We allow patch releases, but the major and minor releases should be within the expiration date\n\t\treturn dates.major >= expiration || dates.minor >= expiration\n\t}\n\n\tprivate isFlagEnabled(flags: number, flag: number) {\n\t\treturn (flags & flag) === flag\n\t}\n\n\tprivate outputNoLicenseKeyProvided() {\n\t\t// Noop, we don't need to show this message.\n\t\t// this.outputMessages([\n\t\t// \t'No tldraw license key provided!',\n\t\t// \t`Please reach out to ${LICENSE_EMAIL} if you would like to license tldraw or if you'd like a trial.`,\n\t\t// ])\n\t}\n\n\tprivate outputInvalidLicenseKey(msg: string) {\n\t\tthis.outputMessages(['Invalid tldraw license key', `Reason: ${msg}`])\n\t}\n\n\tprivate outputLicenseInfoIfNeeded(result: ValidLicenseKeyResult) {\n\t\tif (result.isAnnualLicenseExpired) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'Your tldraw license has expired!',\n\t\t\t\t`Please reach out to ${LICENSE_EMAIL} to renew.`,\n\t\t\t])\n\t\t}\n\n\t\tif (!result.isDomainValid && !result.isDevelopment) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'This tldraw license key is not valid for this domain!',\n\t\t\t\t`Please reach out to ${LICENSE_EMAIL} if you would like to use tldraw on other domains.`,\n\t\t\t])\n\t\t}\n\t\t// If we added a new flag it will be twice the value of the currently highest flag.\n\t\t// And if all the current flags are on we would get the `HIGHEST_FLAG * 2 - 1`, so anything higher than that means there are new flags.\n\t\tif (result.license.flags >= HIGHEST_FLAG * 2) {\n\t\t\tthis.outputMessages([\n\t\t\t\t'This tldraw license contains some unknown flags.',\n\t\t\t\t'You may want to update tldraw packages to a newer version to get access to new functionality.',\n\t\t\t])\n\t\t}\n\t}\n\n\tprivate outputMessages(messages: string[]) {\n\t\tif (this.isTest) return\n\t\tif (this.verbose) {\n\t\t\tthis.outputDelimiter()\n\t\t\tfor (const message of messages) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.log(\n\t\t\t\t\t`%c${message}`,\n\t\t\t\t\t`color: white; background: crimson; padding: 2px; border-radius: 3px;`\n\t\t\t\t)\n\t\t\t}\n\t\t\tthis.outputDelimiter()\n\t\t}\n\t}\n\n\tprivate outputDelimiter() {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.log(\n\t\t\t'%c-------------------------------------------------------------------',\n\t\t\t`color: white; background: crimson; padding: 2px; border-radius: 3px;`\n\t\t)\n\t}\n\n\tstatic className = 'tl-watermark_SEE-LICENSE'\n}\n\nexport function isEditorUnlicensed(result: LicenseFromKeyResult) {\n\tif (!result.isLicenseParseable) return true\n\tif (!result.isDomainValid && !result.isDevelopment) return true\n\tif (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {\n\t\tif (result.isInternalLicense) {\n\t\t\tthrow new Error('License: Internal license expired.')\n\t\t}\n\t\treturn true\n\t}\n\n\treturn false\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "3.13.0-canary.
|
|
1
|
+
const version = "3.13.0-canary.9061a9bbd3fb";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2024-09-13T14:36:29.063Z",
|
|
4
|
-
minor: "2025-04-
|
|
5
|
-
patch: "2025-04-
|
|
4
|
+
minor: "2025-04-22T13:04:46.249Z",
|
|
5
|
+
patch: "2025-04-22T13:04:46.249Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.13.0-canary.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.13.0-canary.9061a9bbd3fb'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-04-22T13:04:46.249Z',\n\tpatch: '2025-04-22T13:04:46.249Z',\n}\n"],
|
|
5
5
|
"mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/editor.css
CHANGED
|
@@ -1710,6 +1710,12 @@ it from receiving any pointer events or affecting the cursor. */
|
|
|
1710
1710
|
background-color: var(--color-primary);
|
|
1711
1711
|
color: var(--color-selected-contrast);
|
|
1712
1712
|
}
|
|
1713
|
+
.tl-container__focused:not(.tl-container__no-focus-ring)
|
|
1714
|
+
.tlui-button.tl-error-boundary__refresh:focus-visible {
|
|
1715
|
+
border-radius: 8px;
|
|
1716
|
+
outline-offset: 0;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1713
1719
|
/* --------------------- Coarse --------------------- */
|
|
1714
1720
|
|
|
1715
1721
|
.tl-hidden {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "A tiny little drawing app (editor).",
|
|
4
|
-
"version": "3.13.0-canary.
|
|
4
|
+
"version": "3.13.0-canary.9061a9bbd3fb",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"@tiptap/core": "^2.9.1",
|
|
49
49
|
"@tiptap/pm": "^2.9.1",
|
|
50
50
|
"@tiptap/react": "^2.9.1",
|
|
51
|
-
"@tldraw/state": "3.13.0-canary.
|
|
52
|
-
"@tldraw/state-react": "3.13.0-canary.
|
|
53
|
-
"@tldraw/store": "3.13.0-canary.
|
|
54
|
-
"@tldraw/tlschema": "3.13.0-canary.
|
|
55
|
-
"@tldraw/utils": "3.13.0-canary.
|
|
56
|
-
"@tldraw/validate": "3.13.0-canary.
|
|
51
|
+
"@tldraw/state": "3.13.0-canary.9061a9bbd3fb",
|
|
52
|
+
"@tldraw/state-react": "3.13.0-canary.9061a9bbd3fb",
|
|
53
|
+
"@tldraw/store": "3.13.0-canary.9061a9bbd3fb",
|
|
54
|
+
"@tldraw/tlschema": "3.13.0-canary.9061a9bbd3fb",
|
|
55
|
+
"@tldraw/utils": "3.13.0-canary.9061a9bbd3fb",
|
|
56
|
+
"@tldraw/validate": "3.13.0-canary.9061a9bbd3fb",
|
|
57
57
|
"@types/core-js": "^2.5.8",
|
|
58
58
|
"@use-gesture/react": "^10.3.1",
|
|
59
59
|
"classnames": "^2.5.1",
|
|
@@ -157,8 +157,10 @@ My browser: ${navigator.userAgent}`
|
|
|
157
157
|
<h2>Are you sure?</h2>
|
|
158
158
|
<p>Resetting your data will delete your drawing and cannot be undone.</p>
|
|
159
159
|
<div className="tl-error-boundary__content__actions">
|
|
160
|
-
<button onClick={() => setShouldShowResetConfirmation(false)}>
|
|
161
|
-
|
|
160
|
+
<button className="tlui-button" onClick={() => setShouldShowResetConfirmation(false)}>
|
|
161
|
+
Cancel
|
|
162
|
+
</button>
|
|
163
|
+
<button className="tlui-button tl-error-boundary__reset" onClick={resetLocalState}>
|
|
162
164
|
Reset data
|
|
163
165
|
</button>
|
|
164
166
|
</div>
|
|
@@ -187,22 +189,24 @@ My browser: ${navigator.userAgent}`
|
|
|
187
189
|
<pre>
|
|
188
190
|
<code>{errorStack ?? errorMessage}</code>
|
|
189
191
|
</pre>
|
|
190
|
-
<button onClick={copyError}>
|
|
192
|
+
<button className="tlui-button" onClick={copyError}>
|
|
193
|
+
{didCopy ? 'Copied!' : 'Copy'}
|
|
194
|
+
</button>
|
|
191
195
|
</div>
|
|
192
196
|
</>
|
|
193
197
|
)}
|
|
194
198
|
<div className="tl-error-boundary__content__actions">
|
|
195
|
-
<button onClick={() => setShouldShowError(!shouldShowError)}>
|
|
199
|
+
<button className="tlui-button" onClick={() => setShouldShowError(!shouldShowError)}>
|
|
196
200
|
{shouldShowError ? 'Hide details' : 'Show details'}
|
|
197
201
|
</button>
|
|
198
202
|
<div className="tl-error-boundary__content__actions__group">
|
|
199
203
|
<button
|
|
200
|
-
className="tl-error-boundary__reset"
|
|
204
|
+
className="tlui-button tl-error-boundary__reset"
|
|
201
205
|
onClick={() => setShouldShowResetConfirmation(true)}
|
|
202
206
|
>
|
|
203
207
|
Reset data
|
|
204
208
|
</button>
|
|
205
|
-
<button className="tl-error-boundary__refresh" onClick={refresh}>
|
|
209
|
+
<button className="tlui-button tl-error-boundary__refresh" onClick={refresh}>
|
|
206
210
|
Refresh Page
|
|
207
211
|
</button>
|
|
208
212
|
</div>
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
TLImageAsset,
|
|
43
43
|
TLInstance,
|
|
44
44
|
TLInstancePageState,
|
|
45
|
+
TLInstancePresence,
|
|
45
46
|
TLNoteShape,
|
|
46
47
|
TLPOINTER_ID,
|
|
47
48
|
TLPage,
|
|
@@ -1704,8 +1705,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1704
1705
|
* @readonly
|
|
1705
1706
|
*/
|
|
1706
1707
|
@computed getSelectedShapes(): TLShape[] {
|
|
1707
|
-
|
|
1708
|
-
return compact(selectedShapeIds.map((id) => this.store.get(id)))
|
|
1708
|
+
return compact(this.getSelectedShapeIds().map((id) => this.store.get(id)))
|
|
1709
1709
|
}
|
|
1710
1710
|
|
|
1711
1711
|
/**
|
|
@@ -2575,14 +2575,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2575
2575
|
return baseCamera
|
|
2576
2576
|
}
|
|
2577
2577
|
|
|
2578
|
+
private _getFollowingPresence(targetUserId: string | null) {
|
|
2579
|
+
const visited = [this.user.getId()]
|
|
2580
|
+
const collaborators = this.getCollaborators()
|
|
2581
|
+
let leaderPresence = null as null | TLInstancePresence
|
|
2582
|
+
while (targetUserId && !visited.includes(targetUserId)) {
|
|
2583
|
+
leaderPresence = collaborators.find((c) => c.userId === targetUserId) ?? null
|
|
2584
|
+
targetUserId = leaderPresence?.followingUserId ?? null
|
|
2585
|
+
if (leaderPresence) {
|
|
2586
|
+
visited.push(leaderPresence.userId)
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return leaderPresence
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2578
2592
|
@computed
|
|
2579
2593
|
private getViewportPageBoundsForFollowing(): null | Box {
|
|
2580
|
-
const
|
|
2581
|
-
if (!followingUserId) return null
|
|
2582
|
-
const leaderPresence = this.getCollaborators().find((c) => c.userId === followingUserId)
|
|
2583
|
-
if (!leaderPresence) return null
|
|
2594
|
+
const leaderPresence = this._getFollowingPresence(this.getInstanceState().followingUserId)
|
|
2584
2595
|
|
|
2585
|
-
if (!leaderPresence
|
|
2596
|
+
if (!leaderPresence?.camera || !leaderPresence?.screenBounds) return null
|
|
2586
2597
|
|
|
2587
2598
|
// Fit their viewport inside of our screen bounds
|
|
2588
2599
|
// 1. calculate their viewport in page space
|
|
@@ -3781,15 +3792,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3781
3792
|
// if we were already following someone, stop following them
|
|
3782
3793
|
this.stopFollowingUser()
|
|
3783
3794
|
|
|
3784
|
-
const leaderPresences = this._getCollaboratorsQuery()
|
|
3785
|
-
.get()
|
|
3786
|
-
.filter((p) => p.userId === userId)
|
|
3787
|
-
|
|
3788
|
-
if (!leaderPresences.length) {
|
|
3789
|
-
console.warn('User not found')
|
|
3790
|
-
return this
|
|
3791
|
-
}
|
|
3792
|
-
|
|
3793
3795
|
const thisUserId = this.user.getId()
|
|
3794
3796
|
|
|
3795
3797
|
if (!thisUserId) {
|
|
@@ -3797,13 +3799,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3797
3799
|
// allow to continue since it's probably fine most of the time.
|
|
3798
3800
|
}
|
|
3799
3801
|
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
+
const leaderPresence = this._getFollowingPresence(userId)
|
|
3803
|
+
|
|
3804
|
+
if (!leaderPresence) {
|
|
3802
3805
|
return this
|
|
3803
3806
|
}
|
|
3804
3807
|
|
|
3805
3808
|
const latestLeaderPresence = computed('latestLeaderPresence', () => {
|
|
3806
|
-
return this.
|
|
3809
|
+
return this._getFollowingPresence(userId)
|
|
3807
3810
|
})
|
|
3808
3811
|
|
|
3809
3812
|
transact(() => {
|
|
@@ -317,6 +317,46 @@ describe('LicenseManager', () => {
|
|
|
317
317
|
expect(result.isDomainValid).toBe(false)
|
|
318
318
|
})
|
|
319
319
|
|
|
320
|
+
it('Succeeds if it is a vscode extension', async () => {
|
|
321
|
+
// @ts-ignore
|
|
322
|
+
delete window.location
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
window.location = new URL(
|
|
325
|
+
'vscode-webview:vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app'
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
329
|
+
permissiveHostsInfo[PROPERTIES.HOSTS] = ['tldraw-org.tldraw-vscode']
|
|
330
|
+
const permissiveLicenseKey = await generateLicenseKey(
|
|
331
|
+
JSON.stringify(permissiveHostsInfo),
|
|
332
|
+
keyPair
|
|
333
|
+
)
|
|
334
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
335
|
+
permissiveLicenseKey
|
|
336
|
+
)) as ValidLicenseKeyResult
|
|
337
|
+
expect(result.isDomainValid).toBe(true)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('Fails if it is a vscode extension with the wrong id', async () => {
|
|
341
|
+
// @ts-ignore
|
|
342
|
+
delete window.location
|
|
343
|
+
// @ts-ignore
|
|
344
|
+
window.location = new URL(
|
|
345
|
+
'vscode-webview:vscode-webview://1ipd8pun8ud7nd7hv9d112g7evi7m10vak9vviuvia66ou6aibp3/index.html?id=6ec2dc7a-afe9-45d9-bd71-1749f9568d28&origin=955b256f-37e1-4a72-a2f4-ad633e88239c&swVersion=4&extensionId=tldraw-org.tldraw-vscode&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app'
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
349
|
+
permissiveHostsInfo[PROPERTIES.HOSTS] = ['blah-org.blah-vscode']
|
|
350
|
+
const permissiveLicenseKey = await generateLicenseKey(
|
|
351
|
+
JSON.stringify(permissiveHostsInfo),
|
|
352
|
+
keyPair
|
|
353
|
+
)
|
|
354
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
355
|
+
permissiveLicenseKey
|
|
356
|
+
)) as ValidLicenseKeyResult
|
|
357
|
+
expect(result.isDomainValid).toBe(false)
|
|
358
|
+
})
|
|
359
|
+
|
|
320
360
|
it('Checks for internal license', async () => {
|
|
321
361
|
const internalLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
322
362
|
internalLicenseInfo[PROPERTIES.FLAGS] = FLAGS.INTERNAL_LICENSE
|
|
@@ -111,7 +111,10 @@ export class LicenseManager {
|
|
|
111
111
|
if (testEnvironment === 'production') return false
|
|
112
112
|
|
|
113
113
|
// If we are using https on a non-localhost domain we assume it's a production env and a development one otherwise
|
|
114
|
-
return
|
|
114
|
+
return (
|
|
115
|
+
!['https:', 'vscode-webview:'].includes(window.location.protocol) ||
|
|
116
|
+
window.location.hostname === 'localhost'
|
|
117
|
+
)
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
|
|
@@ -250,6 +253,15 @@ export class LicenseManager {
|
|
|
250
253
|
return globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`)
|
|
251
254
|
}
|
|
252
255
|
|
|
256
|
+
// VSCode support
|
|
257
|
+
if (window.location.protocol === 'vscode-webview:') {
|
|
258
|
+
const currentUrl = new URL(window.location.href)
|
|
259
|
+
const extensionId = currentUrl.searchParams.get('extensionId')
|
|
260
|
+
if (normalizedHost === extensionId) {
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
253
265
|
return false
|
|
254
266
|
})
|
|
255
267
|
}
|
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 = '3.13.0-canary.
|
|
4
|
+
export const version = '3.13.0-canary.9061a9bbd3fb'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2024-09-13T14:36:29.063Z',
|
|
7
|
-
minor: '2025-04-
|
|
8
|
-
patch: '2025-04-
|
|
7
|
+
minor: '2025-04-22T13:04:46.249Z',
|
|
8
|
+
patch: '2025-04-22T13:04:46.249Z',
|
|
9
9
|
}
|