Introduce CSP (#3512)
This commit is contained in:
parent
c3e86fb157
commit
42fe08cb6e
@ -3,8 +3,10 @@
|
|||||||
cp -R ./public/* ${1:-"dist"}
|
cp -R ./public/* ${1:-"dist"}
|
||||||
|
|
||||||
cp ./src/lib/rlottie/rlottie-wasm.wasm ${1:-"dist"}
|
cp ./src/lib/rlottie/rlottie-wasm.wasm ${1:-"dist"}
|
||||||
|
|
||||||
cp ./src/lib/video-preview/libav-3.10.5.1.2-webcodecs.wasm.js ${1:-"dist"}
|
cp ./src/lib/video-preview/libav-3.10.5.1.2-webcodecs.wasm.js ${1:-"dist"}
|
||||||
cp ./src/lib/video-preview/libav-3.10.5.1.2-webcodecs.wasm.wasm ${1:-"dist"}
|
cp ./src/lib/video-preview/libav-3.10.5.1.2-webcodecs.wasm.wasm ${1:-"dist"}
|
||||||
|
|
||||||
cp ./src/lib/webp/webp_wasm.wasm ${1:-"dist"}
|
cp ./src/lib/webp/webp_wasm.wasm ${1:-"dist"}
|
||||||
|
|
||||||
cp ./node_modules/opus-recorder/dist/decoderWorker.min.wasm ${1:-"dist"}
|
cp ./node_modules/opus-recorder/dist/decoderWorker.min.wasm ${1:-"dist"}
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@ -19478,6 +19478,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/raw-loader": {
|
"node_modules/raw-loader": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/rc": {
|
"node_modules/rc": {
|
||||||
@ -20204,8 +20206,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/script-loader": {
|
"node_modules/script-loader": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/script-loader/-/script-loader-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-UMNLEvgOAQuzK8ji8qIscM3GIrRCWN6MmMXGD4SD5l6cSycgGsCo0tX5xRnfQcoghqct0tjHjcykgI1PyBE2aA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"raw-loader": "~0.5.1"
|
"raw-loader": "~0.5.1"
|
||||||
}
|
}
|
||||||
|
|||||||
25
public/compatTest.js
Normal file
25
public/compatTest.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
function compatTest() {
|
||||||
|
var hasPromise = typeof Promise !== 'undefined';
|
||||||
|
var hasWebSockets = typeof WebSocket !== 'undefined';
|
||||||
|
var hasWebCrypto = window.crypto && typeof window.crypto.subtle !== 'undefined';
|
||||||
|
var hasObjectFromEntries = typeof Object.fromEntries !== 'undefined';
|
||||||
|
|
||||||
|
var isCompatible = hasPromise && hasWebSockets && hasWebCrypto && hasObjectFromEntries;
|
||||||
|
|
||||||
|
if (isCompatible || (window.localStorage && window.localStorage.getItem('tt-ignore-compat'))) {
|
||||||
|
window.isCompatTestPassed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.console && console.warn) {
|
||||||
|
console.warn('Compatibility test report:');
|
||||||
|
console.warn('Promise', hasPromise);
|
||||||
|
console.warn('WebSocket', hasWebSockets);
|
||||||
|
console.warn('WebCrypto', hasWebCrypto);
|
||||||
|
console.warn('Object.fromEntries', hasObjectFromEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.innerHTML = '<iframe src="./unsupported.html" width="100%" height="100%">';
|
||||||
|
}
|
||||||
|
|
||||||
|
compatTest();
|
||||||
11
public/redirect.js
Normal file
11
public/redirect.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const { pathname, hostname, href } = window.location;
|
||||||
|
|
||||||
|
if (pathname.startsWith('/z')) {
|
||||||
|
window.location.href = href.replace('/z', '/a');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(hostname === 'weba.telegram.org' || hostname === 'webz.telegram.org') && !localStorage.getItem('tt-global-state')
|
||||||
|
) {
|
||||||
|
window.location.href = 'https://web.telegram.org/a';
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ export const DEBUG_MORE = false;
|
|||||||
export const STRICTERDOM_ENABLED = DEBUG && !IS_ELECTRON;
|
export const STRICTERDOM_ENABLED = DEBUG && !IS_ELECTRON;
|
||||||
|
|
||||||
export const BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20';
|
export const BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20';
|
||||||
export const ELECTRON_HOST_URL = 'https://telegram-a-host';
|
export const ELECTRON_HOST_URL = process.env.ELECTRON_HOST_URL!;
|
||||||
|
|
||||||
export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.';
|
export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.';
|
||||||
export const DEBUG_GRAMJS = false;
|
export const DEBUG_GRAMJS = false;
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Telegram is a cloud-based mobile and desktop messaging app with a focus on security and speed.">
|
content="Telegram is a cloud-based mobile and desktop messaging app with a focus on security and speed.">
|
||||||
<meta name="viewport"
|
<meta name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover">
|
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-title" content="<%= htmlWebpackPlugin.options.appTitle %>">
|
<meta name="mobile-web-app-title" content="<%= htmlWebpackPlugin.options.appTitle %>">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
@ -23,12 +23,14 @@
|
|||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<meta name="google" content="notranslate">
|
<meta name="google" content="notranslate">
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="<%= htmlWebpackPlugin.options.csp %>">
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="<%= htmlWebpackPlugin.options.baseUrl %>">
|
<meta property="og:url" content="<%= htmlWebpackPlugin.options.baseUrl %>">
|
||||||
<meta property="og:title" content="<%= htmlWebpackPlugin.options.appTitle %>">
|
<meta property="og:title" content="<%= htmlWebpackPlugin.options.appTitle %>">
|
||||||
<meta property="og:description"
|
<meta property="og:description"
|
||||||
content="Telegram is a cloud-based mobile and desktop messaging app with a focus on security and speed.">
|
content="Telegram is a cloud-based mobile and desktop messaging app with a focus on security and speed.">
|
||||||
<meta property="og:image" content="./<%= htmlWebpackPlugin.options.mainIcon %>.png">
|
<meta property="og:image" content="./<%= htmlWebpackPlugin.options.mainIcon %>.png">
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
@ -48,17 +50,8 @@
|
|||||||
<link rel="alternate icon" href="./favicon.ico" type="image/x-icon">
|
<link rel="alternate icon" href="./favicon.ico" type="image/x-icon">
|
||||||
<link rel="manifest" id="the-manifest-placeholder" href="./<%= htmlWebpackPlugin.options.manifest %>">
|
<link rel="manifest" id="the-manifest-placeholder" href="./<%= htmlWebpackPlugin.options.manifest %>">
|
||||||
|
|
||||||
<script>
|
<script src="./compatTest.js"></script>
|
||||||
const { pathname, hostname, href } = window.location;
|
<script src="./redirect.js"></script>
|
||||||
|
|
||||||
if (pathname.startsWith('/z')) {
|
|
||||||
window.location.href = href.replace('/z', '/a');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((hostname === 'weba.telegram.org' || hostname === 'webz.telegram.org') && !localStorage.getItem('tt-global-state')) {
|
|
||||||
window.location.href = 'https://web.telegram.org/a';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="root">
|
<body id="root">
|
||||||
@ -68,33 +61,6 @@
|
|||||||
<p>Please, enable JavaScript to open the app.</p>
|
<p>Please, enable JavaScript to open the app.</p>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="portals"></div>
|
<div id="portals"></div>
|
||||||
<script>
|
|
||||||
function compatTest() {
|
|
||||||
var hasPromise = typeof Promise !== 'undefined';
|
|
||||||
var hasWebSockets = typeof WebSocket !== 'undefined';
|
|
||||||
var hasWebCrypto = window.crypto && typeof window.crypto.subtle !== 'undefined';
|
|
||||||
var hasObjectFromEntries = typeof Object.fromEntries !== 'undefined';
|
|
||||||
|
|
||||||
var isCompatible = hasPromise && hasWebSockets && hasWebCrypto && hasObjectFromEntries;
|
|
||||||
|
|
||||||
if (isCompatible || (window.localStorage && window.localStorage.getItem('tt-ignore-compat'))) {
|
|
||||||
window.isCompatTestPassed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.console && console.warn) {
|
|
||||||
console.warn('Compatibility test report:');
|
|
||||||
console.warn('Promise', hasPromise);
|
|
||||||
console.warn('WebSocket', hasWebSockets);
|
|
||||||
console.warn('WebCrypto', hasWebCrypto);
|
|
||||||
console.warn('Object.fromEntries', hasObjectFromEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.innerHTML = '<iframe src="./unsupported.html" width="100%" height="100%">';
|
|
||||||
}
|
|
||||||
|
|
||||||
compatTest();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -2,7 +2,7 @@ import { inflate } from 'pako/dist/pako_inflate';
|
|||||||
import { createWorkerInterface } from '../../util/createPostMessageInterface';
|
import { createWorkerInterface } from '../../util/createPostMessageInterface';
|
||||||
import type { CancellableCallback } from '../../util/PostMessageConnector';
|
import type { CancellableCallback } from '../../util/PostMessageConnector';
|
||||||
|
|
||||||
import 'script-loader!./rlottie-wasm';
|
importScripts(new URL('./rlottie-wasm.js', import.meta.url));
|
||||||
|
|
||||||
declare const Module: any;
|
declare const Module: any;
|
||||||
|
|
||||||
|
|||||||
@ -75,12 +75,13 @@ function destroy() {
|
|||||||
|
|
||||||
async function loadLibAV() {
|
async function loadLibAV() {
|
||||||
if (isLoaded) return;
|
if (isLoaded) return;
|
||||||
// @ts-ignore
|
|
||||||
await import('script-loader!./libav-3.10.5.1.2-webcodecs');
|
importScripts(new URL('./libav-3.10.5.1.2-webcodecs.js', import.meta.url));
|
||||||
await LibAVWebCodecs.load({
|
await LibAVWebCodecs.load({
|
||||||
polyfill: true,
|
polyfill: true,
|
||||||
libavOptions: { noworker: true, nosimd: true },
|
libavOptions: { noworker: true, nosimd: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import 'script-loader!./webp_wasm';
|
importScripts(new URL('./webp_wasm.js', import.meta.url));
|
||||||
|
|
||||||
Module.onRuntimeInitialized = async () => {
|
Module.onRuntimeInitialized = async () => {
|
||||||
self.postMessage({ type: 'initialized' });
|
self.postMessage({ type: 'initialized' });
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { DEBUG } from './config';
|
import { DEBUG, ELECTRON_HOST_URL, IS_ELECTRON } from '../config';
|
||||||
import { respondForProgressive } from './serviceWorker/progressive';
|
import { respondForProgressive } from './progressive';
|
||||||
import { respondForDownload } from './serviceWorker/download';
|
import { respondForDownload } from './download';
|
||||||
import { respondWithCache, clearAssetCache, respondWithCacheNetworkFirst } from './serviceWorker/assetCache';
|
import { respondWithCache, clearAssetCache, respondWithCacheNetworkFirst } from './assetCache';
|
||||||
import {
|
import {
|
||||||
handlePush,
|
handlePush,
|
||||||
handleNotificationClick,
|
handleNotificationClick,
|
||||||
handleClientMessage as handleNotificationMessage,
|
handleClientMessage as handleNotificationMessage,
|
||||||
} from './serviceWorker/pushNotification';
|
} from './pushNotification';
|
||||||
import { respondForShare, handleClientMessage as handleShareMessage } from './serviceWorker/share';
|
import { respondForShare, handleClientMessage as handleShareMessage } from './share';
|
||||||
|
|
||||||
import { pause } from './util/schedulers';
|
import { pause } from '../util/schedulers';
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
@ -48,28 +48,35 @@ self.addEventListener('activate', (e) => {
|
|||||||
|
|
||||||
self.addEventListener('fetch', (e: FetchEvent) => {
|
self.addEventListener('fetch', (e: FetchEvent) => {
|
||||||
const { url } = e.request;
|
const { url } = e.request;
|
||||||
|
const scope = IS_ELECTRON ? ELECTRON_HOST_URL : self.registration.scope;
|
||||||
|
if (!url.startsWith(scope)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.includes('/progressive/')) {
|
const { pathname, protocol } = new URL(url);
|
||||||
|
const { pathname: scopePathname } = new URL(scope);
|
||||||
|
|
||||||
|
if (pathname.startsWith('/progressive/')) {
|
||||||
e.respondWith(respondForProgressive(e));
|
e.respondWith(respondForProgressive(e));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.includes('/download/')) {
|
if (pathname.startsWith('/download/')) {
|
||||||
e.respondWith(respondForDownload(e));
|
e.respondWith(respondForDownload(e));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.includes('/share/')) {
|
if (pathname.startsWith('/share/')) {
|
||||||
e.respondWith(respondForShare(e));
|
e.respondWith(respondForShare(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith('http')) {
|
if (protocol === 'http:' || protocol === 'https:') {
|
||||||
if (new URL(url).pathname === '/' || url.match(RE_NETWORK_FIRST_ASSETS)) {
|
if (pathname === scopePathname || pathname.match(RE_NETWORK_FIRST_ASSETS)) {
|
||||||
e.respondWith(respondWithCacheNetworkFirst(e));
|
e.respondWith(respondWithCacheNetworkFirst(e));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.match(RE_CACHE_FIRST_ASSETS)) {
|
if (pathname.match(RE_CACHE_FIRST_ASSETS)) {
|
||||||
e.respondWith(respondWithCache(e));
|
e.respondWith(respondWithCache(e));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { IS_ELECTRON, ELECTRON_HOST_URL } from '../config';
|
import { ELECTRON_HOST_URL, IS_ELECTRON } from '../config';
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const cacheApi = self.caches;
|
const cacheApi = self.caches;
|
||||||
|
|||||||
@ -60,7 +60,7 @@ if (IS_SERVICE_WORKER_SUPPORTED) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigator.serviceWorker.register(new URL('../serviceWorker.ts', import.meta.url));
|
await navigator.serviceWorker.register(new URL('../serviceWorker', import.meta.url));
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { hasStoredSession } from './sessions';
|
|||||||
const WEBSYNC_URLS = [
|
const WEBSYNC_URLS = [
|
||||||
't.me',
|
't.me',
|
||||||
'telegram.me',
|
'telegram.me',
|
||||||
].map((domain) => `//${domain}/_websync_?`);
|
].map((domain) => `https://${domain}/_websync_?`);
|
||||||
const WEBSYNC_VERSION = `${APP_VERSION} Z`;
|
const WEBSYNC_VERSION = `${APP_VERSION} Z`;
|
||||||
const WEBSYNC_KEY = 'tgme_sync';
|
const WEBSYNC_KEY = 'tgme_sync';
|
||||||
const WEBSYNC_TIMEOUT = 86400;
|
const WEBSYNC_TIMEOUT = 86400;
|
||||||
|
|||||||
@ -27,15 +27,30 @@ const {
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const STATOSCOPE_REFERENCE_URL = 'https://tga.dev/build-stats.json';
|
|
||||||
let isReferenceFetched = false;
|
|
||||||
const DEFAULT_APP_TITLE = `Telegram${APP_ENV !== 'production' ? ' Beta' : ''}`;
|
const DEFAULT_APP_TITLE = `Telegram${APP_ENV !== 'production' ? ' Beta' : ''}`;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
BASE_URL = 'https://web.telegram.org/a/',
|
BASE_URL = 'https://web.telegram.org/a/',
|
||||||
|
ELECTRON_HOST_URL = 'https://telegram-a-host',
|
||||||
APP_TITLE = DEFAULT_APP_TITLE,
|
APP_TITLE = DEFAULT_APP_TITLE,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
|
const CSP = `
|
||||||
|
default-src 'self';
|
||||||
|
connect-src 'self' wss://*.web.telegram.org blob: http: https:;
|
||||||
|
script-src 'self' 'wasm-unsafe-eval' https://t.me/_websync_ https://telegram.me/_websync_;
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: blob: https://ss3.4sqi.net/img/categories_v2/ ${IS_ELECTRON ? BASE_URL : ''};
|
||||||
|
media-src 'self' blob: data: ${IS_ELECTRON ? [BASE_URL, ELECTRON_HOST_URL].join(' ') : ''};
|
||||||
|
object-src 'none';
|
||||||
|
frame-src http: https:;
|
||||||
|
base-uri 'none';
|
||||||
|
form-action 'none';`
|
||||||
|
.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
const STATOSCOPE_REFERENCE_URL = 'https://tga.dev/build-stats.json';
|
||||||
|
let isReferenceFetched = false;
|
||||||
|
|
||||||
export default function createConfig(
|
export default function createConfig(
|
||||||
_: any,
|
_: any,
|
||||||
{ mode = 'production' }: { mode: 'none' | 'development' | 'production' },
|
{ mode = 'production' }: { mode: 'none' | 'development' | 'production' },
|
||||||
@ -76,6 +91,9 @@ export default function createConfig(
|
|||||||
devMiddleware: {
|
devMiddleware: {
|
||||||
stats: 'minimal',
|
stats: 'minimal',
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Security-Policy': CSP,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
@ -180,6 +198,7 @@ export default function createConfig(
|
|||||||
mainIcon: APP_ENV === 'production' ? 'icon-192x192' : 'icon-dev-192x192',
|
mainIcon: APP_ENV === 'production' ? 'icon-192x192' : 'icon-dev-192x192',
|
||||||
manifest: APP_ENV === 'production' ? 'site.webmanifest' : 'site_dev.webmanifest',
|
manifest: APP_ENV === 'production' ? 'site.webmanifest' : 'site_dev.webmanifest',
|
||||||
baseUrl: BASE_URL,
|
baseUrl: BASE_URL,
|
||||||
|
csp: CSP,
|
||||||
template: 'src/index.html',
|
template: 'src/index.html',
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
@ -199,7 +218,9 @@ export default function createConfig(
|
|||||||
TELEGRAM_T_API_HASH: undefined,
|
TELEGRAM_T_API_HASH: undefined,
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
TEST_SESSION: null,
|
TEST_SESSION: null,
|
||||||
|
ELECTRON_HOST_URL,
|
||||||
}),
|
}),
|
||||||
|
// Updates each dev re-build to provide current git branch or commit hash
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
APP_VERSION: JSON.stringify(appVersion),
|
APP_VERSION: JSON.stringify(appVersion),
|
||||||
APP_REVISION: DefinePlugin.runtimeValue(() => {
|
APP_REVISION: DefinePlugin.runtimeValue(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user