Skip to content

Commit

Permalink
Add support for InTune Single-Sign-On (#1280)
Browse files Browse the repository at this point in the history
This add support for communicating with the Microsoft Authentication
Broker over its DBus interface in order to retrieve an authentication
cookie, that can be used to automatically login the user currently
logged-in via InTune. This also adds support for MFA and Conditional
Access, which allows use of Teams outside of corporate network in case
the organization has chosen to only allow access from registered
devices.

Behind the scene this uses the same mechanism as Microsoft Edge on
Linux: upon loading a website from login.microsoftonline.com the URL
is passed to the authentication broker in order to prepare a token
based on the PRT (Primary Refresh Token). The returned refresh token
is passed to the server via the 'X-Ms-Refreshtokencredential' HTTP
header. With this token in place the server will skip any interactive
prompts and generate a proper OAuth authentication token. Since the
PRT is tied to the device credentials, the resulting refresh token
carries the MFA attribute, which causes it to be accepted even if the
Conditional Access policy mandates strong, device-based
authentication.

Signed-off-by: Krzysztof Nowicki <[email protected]>
Co-authored-by: Krzysztof Nowicki <[email protected]>
  • Loading branch information
KrissN and Krzysztof Nowicki authored Jun 4, 2024
1 parent 191a64a commit 0896927
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 6 deletions.
2 changes: 2 additions & 0 deletions app/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Here is the list of available arguments and its usage:
| spellCheckerLanguages | Array of languages to use with Electron's spell checker | [] |
| ssoBasicAuthUser | Login that will be sent for basic_auth SSO login. | string |
| ssoBasicAuthPasswordCommand | Command to execute, grab stdout and use it as a password for basic_auth SSO login. | string |
| ssoIntuneEnabled | Enable InTune Single-Sign-On | false
| ssoIntuneAuthUser | User (e-mail) to be used for InTune SSO login. | string |
| trayIconEnabled | Enable tray icon | true |
| url | Microsoft Teams URL | string |
| useMutationTitleLogic | Use MutationObserver to update counter from title | true |
Expand Down
10 changes: 10 additions & 0 deletions app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,16 @@ function argv(configPath, appVersion) {
describe: 'Command to execute to retrieve password for SSO basic auth.',
type: 'string'
},
ssoInTuneEnabled: {
default: false,
describe: 'Enable Single-Sign-On using Microsoft InTune.',
type: 'boolean'
},
ssoInTuneAuthUser: {
default: '',
describe: 'User (e-mail) to use for InTune SSO.',
type: 'string'
},
trayIconEnabled: {
default: true,
describe: 'Enable tray icon',
Expand Down
91 changes: 91 additions & 0 deletions app/intune/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const dbus = require('@homebridge/dbus-native');
const { LucidLog } = require('lucid-log');

var sessionBus = dbus.sessionBus();

var intuneAccount = null;

var brokerService = sessionBus.getService('com.microsoft.identity.broker1');

function processInTuneAccounts(logger, resp, ssoInTuneAuthUser) {
response = JSON.parse(resp);
if ('error' in response) {
logger.warn('Failed to retrieve InTune account list: ' + response.error.context);
return;
};

if (ssoInTuneAuthUser == '') {
intuneAccount = response.accounts[0];
logger.debug('Using first available InTune account (' + intuneAccount.username + ')');
} else {
for (account in response.accounts) {
if (account.username == ssoIntuneAuthUser) {
intuneAccount = account;
logger.debug('Found matching InTune account (' + intuneAccount.username + ')');
break;
}
}
if (intuneAccount == null) {
logger.warn('Failed to find matching InTune account for ' + ssoIntuneAuthUser + '.');
}
}
}

exports.initSso = function initIntuneSso(logger, ssoInTuneAuthUser) {
logger.debug("Initializing InTune SSO");
brokerService.getInterface(
'/com/microsoft/identity/broker1',
'com.microsoft.identity.Broker1', function(err, broker) {
if (err) {
logger.warn('Failed to find microsoft-identity-broker DBus interface');
return;
}
broker.getAccounts('0.0', '', JSON.stringify({'clientId': '88200948-af09-45a1-9c03-53cdcc75c183', 'redirectUri':'urn:ietf:oob'}), function(err, resp) {
if (err) {
logger.warn('Failed to communicate with microsoft-identity-broker');
return;
}
processInTuneAccounts(logger, resp, ssoInTuneAuthUser);
});
});
}

exports.setupUrlFilter = function setupUrlFilter(filter) {
filter.urls.push('https://login.microsoftonline.com/*');
}

exports.isSsoUrl = function isSsoUrl(url) {
return intuneAccount != null && url.startsWith('https://login.microsoftonline.com/');
}

function processPrtResponse(logger, resp, detail) {
response = JSON.parse(resp);
if ('error' in response) {
logger.warn('Failed to retrieve Intune SSO cookie: ' + response.error.context);
} else {
logger.debug('Adding SSO credential');
detail.requestHeaders['X-Ms-Refreshtokencredential'] = response['cookieContent'];
}
}

exports.addSsoCookie = function addIntuneSsoCookie(logger, detail, callback) {
logger.debug('Retrieving InTune SSO cookie');
if (intuneAccount == null) {
logger.info("InTune SSO not active");
callback({
requestHeaders: detail.requestHeaders
});
return;
}
brokerService.getInterface(
'/com/microsoft/identity/broker1',
'com.microsoft.identity.Broker1', function(err, broker) {
broker.acquirePrtSsoCookie('0.0', '', JSON.stringify({'ssoUrl':detail.url, 'account':intuneAccount, 'authParameters':{'authority':'https://login.microsoftonline.com/common/'}}), function(err, resp) {
processPrtResponse(logger, resp, detail);
callback({
requestHeaders: detail.requestHeaders
});
});
});
}

20 changes: 15 additions & 5 deletions app/mainAppWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TrayIconChooser = require('../browser/tools/trayIconChooser');
const { AppConfiguration } = require('../appConfiguration');
const connMgr = require('../connectionManager');
const fs = require('fs');
const intune = require('../intune');

/**
* @type {TrayIconChooser}
Expand Down Expand Up @@ -65,6 +66,10 @@ exports.onAppReady = async function onAppReady(configGroup) {
levels: config.appLogLevels.split(',')
});

if (config.ssoInTuneEnabled) {
intune.initSso(logger, config.ssoInTuneAuthUser);
}

window = await createWindow();

if (config.trayIconEnabled) {
Expand Down Expand Up @@ -323,12 +328,16 @@ function setImgSrcSecurityPolicy(policies) {
* @param {Electron.BeforeSendResponse} callback
*/
function onBeforeSendHeadersHandler(detail, callback) {
if (detail.url.startsWith(customBGServiceUrl.href)) {
detail.requestHeaders['Access-Control-Allow-Origin'] = '*';
if (intune.isSsoUrl(detail.url)) {
intune.addSsoCookie(logger, detail, callback);
} else {
if (detail.url.startsWith(customBGServiceUrl.href)) {
detail.requestHeaders['Access-Control-Allow-Origin'] = '*';
}
callback({
requestHeaders: detail.requestHeaders
});
}
callback({
requestHeaders: detail.requestHeaders
});
}

/**
Expand Down Expand Up @@ -409,6 +418,7 @@ function addEventHandlers() {

function getWebRequestFilterFromURL() {
const filter = customBGServiceUrl.protocol === 'http:' ? { urls: ['http://*/*'] } : { urls: ['https://*/*'] };
intune.setupUrlFilter(filter);
return filter;
}

Expand Down
7 changes: 7 additions & 0 deletions com.github.IsmaelMartinez.teams_for_linux.appdata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
<url type="bugtracker">https://github.com/IsmaelMartinez/teams-for-linux/issues</url>
<launchable type="desktop-id">com.github.IsmaelMartinez.teams_for_linux.desktop</launchable>
<releases>
<release version="1.5.3" date="2024-06-04">
<description>
<ul>
<li>Add support for Single Sign On using Microsoft Intune</li>
</ul>
</description>
</release>
<release version="1.5.2" date="2024-05-28">
<description>
<ul>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "teams-for-linux",
"version": "1.5.2",
"version": "1.5.3",
"main": "app/index.js",
"description": "Unofficial client for Microsoft Teams for Linux",
"homepage": "https://github.com/IsmaelMartinez/teams-for-linux",
Expand Down Expand Up @@ -40,6 +40,7 @@
"release": "electron-builder"
},
"dependencies": {
"@homebridge/dbus-native": "0.6.0",
"@electron/remote": "^2.1.2",
"electron-is-dev": "2.0.0",
"electron-store": "8.2.0",
Expand Down
106 changes: 106 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,29 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691"
integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==

"@homebridge/[email protected]":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@homebridge/dbus-native/-/dbus-native-0.6.0.tgz#25052dd03216b977298e4c9db19b7aeb1d2363f4"
integrity sha512-xObqQeYHTXmt6wsfj10+krTo4xbzR9BgUfX2aQ+edDC9nc4ojfzLScfXCh3zluAm6UCowKw+AFfXn6WLWUOPkg==
dependencies:
"@homebridge/long" "^5.2.1"
"@homebridge/put" "^0.0.8"
event-stream "^4.0.1"
hexy "^0.3.5"
minimist "^1.2.6"
safe-buffer "^5.1.2"
xml2js "^0.6.2"

"@homebridge/long@^5.2.1":
version "5.2.1"
resolved "https://registry.yarnpkg.com/@homebridge/long/-/long-5.2.1.tgz#1c7568775b78e1a0fd75a7b3fa7a995f0388ab37"
integrity sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA==

"@homebridge/put@^0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@homebridge/put/-/put-0.0.8.tgz#4b8b99f2c4d58bc762718863699df2c5bc0b4b8a"
integrity sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA==

"@humanwhocodes/config-array@^0.12.3":
version "0.12.3"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.12.3.tgz#a6216d90f81a30bedd1d4b5d799b47241f318072"
Expand Down Expand Up @@ -795,6 +818,11 @@ dotenv@^9.0.2:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05"
integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==

duplexer@^0.1.1, duplexer@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==

eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
Expand Down Expand Up @@ -1012,6 +1040,19 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==

event-stream@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65"
integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==
dependencies:
duplexer "^0.1.1"
from "^0.1.7"
map-stream "0.0.7"
pause-stream "^0.0.11"
split "^1.0.1"
stream-combiner "^0.2.2"
through "^2.3.8"

extract-zip@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
Expand Down Expand Up @@ -1116,6 +1157,11 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"

from@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==

fs-extra@^10.0.0, fs-extra@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
Expand Down Expand Up @@ -1302,6 +1348,11 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"

hexy@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/hexy/-/hexy-0.3.5.tgz#adcd5ee47d66aca3581d771743a509a5176e45f9"
integrity sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==

hosted-git-info@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224"
Expand Down Expand Up @@ -1585,6 +1636,11 @@ lucid-log@^0.0.3:
dependencies:
chalk "^4.1.2"

[email protected]:
version "0.0.7"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8"
integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==

matcher@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca"
Expand Down Expand Up @@ -1821,6 +1877,13 @@ path-scurry@^1.10.2:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"

pause-stream@^0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==
dependencies:
through "~2.3"

pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
Expand Down Expand Up @@ -1951,6 +2014,11 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"

safe-buffer@^5.1.2:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==

"safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
Expand All @@ -1963,6 +2031,11 @@ sanitize-filename@^1.6.3:
dependencies:
truncate-utf8-bytes "^1.0.0"

sax@>=0.6.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f"
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==

sax@^1.2.4:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
Expand Down Expand Up @@ -2043,6 +2116,13 @@ source-map@^0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==

split@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
dependencies:
through "2"

sprintf-js@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
Expand All @@ -2053,6 +2133,14 @@ stat-mode@^1.0.0:
resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==

stream-combiner@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858"
integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==
dependencies:
duplexer "~0.1.1"
through "~2.3.4"

"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
Expand Down Expand Up @@ -2129,6 +2217,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==

through@2, through@^2.3.8, through@~2.3, through@~2.3.4:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==

tmp-promise@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"
Expand Down Expand Up @@ -2241,11 +2334,24 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==

xml2js@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499"
integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==
dependencies:
sax ">=0.6.0"
xmlbuilder "~11.0.0"

xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==

xmlbuilder@~11.0.0:
version "11.0.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==

y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
Expand Down

0 comments on commit 0896927

Please sign in to comment.