mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-24 17:16:03 +00:00
support encrypted private ssh keys (fixes #262)
This commit is contained in:
@@ -10,6 +10,7 @@ import { SSHModalComponent } from './components/sshModal.component'
|
|||||||
import { PromptModalComponent } from './components/promptModal.component'
|
import { PromptModalComponent } from './components/promptModal.component'
|
||||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||||
import { SSHService } from './services/ssh.service'
|
import { SSHService } from './services/ssh.service'
|
||||||
|
import { PasswordStorageService } from './services/passwordStorage.service'
|
||||||
|
|
||||||
import { ButtonProvider } from './buttonProvider'
|
import { ButtonProvider } from './buttonProvider'
|
||||||
import { SSHConfigProvider } from './config'
|
import { SSHConfigProvider } from './config'
|
||||||
@@ -22,6 +23,7 @@ import { SSHSettingsTabProvider } from './settings'
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
PasswordStorageService,
|
||||||
SSHService,
|
SSHService,
|
||||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||||
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
|
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
|
||||||
|
73
terminus-ssh/src/services/passwordStorage.service.ts
Normal file
73
terminus-ssh/src/services/passwordStorage.service.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
|
import { SSHConnection } from '../api'
|
||||||
|
|
||||||
|
let xkeychain
|
||||||
|
let wincredmgr
|
||||||
|
try {
|
||||||
|
xkeychain = require('xkeychain')
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
wincredmgr = require('wincredmgr')
|
||||||
|
} catch (error2) {
|
||||||
|
console.warn('No keychain manager available')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PasswordStorageService {
|
||||||
|
constructor (
|
||||||
|
private zone: NgZone,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
savePassword (connection: SSHConnection, password: string) {
|
||||||
|
if (xkeychain) {
|
||||||
|
xkeychain.setPassword({
|
||||||
|
account: connection.user,
|
||||||
|
service: `ssh@${connection.host}`,
|
||||||
|
password
|
||||||
|
}, () => null)
|
||||||
|
} else {
|
||||||
|
wincredmgr.WriteCredentials(
|
||||||
|
'user',
|
||||||
|
password,
|
||||||
|
`ssh:${connection.user}@${connection.host}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePassword (connection: SSHConnection) {
|
||||||
|
if (xkeychain) {
|
||||||
|
xkeychain.deletePassword({
|
||||||
|
account: connection.user,
|
||||||
|
service: `ssh@${connection.host}`,
|
||||||
|
}, () => null)
|
||||||
|
} else {
|
||||||
|
wincredmgr.DeleteCredentials(
|
||||||
|
`ssh:${connection.user}@${connection.host}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPassword (connection: SSHConnection): Promise<string> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (!wincredmgr && !xkeychain.isSupported()) {
|
||||||
|
return resolve(null)
|
||||||
|
}
|
||||||
|
if (xkeychain) {
|
||||||
|
xkeychain.getPassword(
|
||||||
|
{
|
||||||
|
account: connection.user,
|
||||||
|
service: `ssh@${connection.host}`,
|
||||||
|
},
|
||||||
|
(_, result) => this.zone.run(() => resolve(result))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password)
|
||||||
|
} catch (error) {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -3,92 +3,59 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { Client } from 'ssh2'
|
import { Client } from 'ssh2'
|
||||||
import * as fs from 'mz/fs'
|
import * as fs from 'mz/fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { AppService, HostAppService, Platform } from 'terminus-core'
|
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
|
||||||
import { TerminalTabComponent } from 'terminus-terminal'
|
import { TerminalTabComponent } from 'terminus-terminal'
|
||||||
import { SSHConnection, SSHSession } from '../api'
|
import { SSHConnection, SSHSession } from '../api'
|
||||||
import { PromptModalComponent } from '../components/promptModal.component'
|
import { PromptModalComponent } from '../components/promptModal.component'
|
||||||
|
import { PasswordStorageService } from './passwordStorage.service'
|
||||||
const { SSH2Stream } = require('ssh2-streams')
|
const { SSH2Stream } = require('ssh2-streams')
|
||||||
|
|
||||||
let xkeychain
|
|
||||||
let wincredmgr
|
|
||||||
try {
|
|
||||||
xkeychain = require('xkeychain')
|
|
||||||
} catch (error) {
|
|
||||||
try {
|
|
||||||
wincredmgr = require('wincredmgr')
|
|
||||||
} catch (error2) {
|
|
||||||
console.warn('No keychain manager available')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SSHService {
|
export class SSHService {
|
||||||
|
private logger: Logger
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
log: LogService,
|
||||||
private app: AppService,
|
private app: AppService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
|
private passwordStorage: PasswordStorageService,
|
||||||
) {
|
) {
|
||||||
}
|
this.logger = log.create('ssh')
|
||||||
|
|
||||||
savePassword (connection: SSHConnection, password: string) {
|
|
||||||
if (xkeychain) {
|
|
||||||
xkeychain.setPassword({
|
|
||||||
account: connection.user,
|
|
||||||
service: `ssh@${connection.host}`,
|
|
||||||
password
|
|
||||||
}, () => null)
|
|
||||||
} else {
|
|
||||||
wincredmgr.WriteCredentials(
|
|
||||||
'user',
|
|
||||||
password,
|
|
||||||
`ssh:${connection.user}@${connection.host}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePassword (connection: SSHConnection) {
|
|
||||||
if (xkeychain) {
|
|
||||||
xkeychain.deletePassword({
|
|
||||||
account: connection.user,
|
|
||||||
service: `ssh@${connection.host}`,
|
|
||||||
}, () => null)
|
|
||||||
} else {
|
|
||||||
wincredmgr.DeleteCredentials(
|
|
||||||
`ssh:${connection.user}@${connection.host}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPassword (connection: SSHConnection): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (xkeychain) {
|
|
||||||
xkeychain.getPassword({
|
|
||||||
account: connection.user,
|
|
||||||
service: `ssh@${connection.host}`,
|
|
||||||
}, (_, result) => resolve(result))
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password)
|
|
||||||
} catch (error) {
|
|
||||||
resolve(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
|
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
|
||||||
let privateKey: string = null
|
let privateKey: string = null
|
||||||
let keyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
let privateKeyPassphrase: string = null
|
||||||
if (!connection.privateKey && await fs.exists(keyPath)) {
|
let privateKeyPath = connection.privateKey
|
||||||
connection.privateKey = keyPath
|
if (!privateKeyPath) {
|
||||||
|
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
|
||||||
|
if (await fs.exists(userKeyPath)) {
|
||||||
|
this.logger.info('Using user\'s default private key:', userKeyPath)
|
||||||
|
privateKeyPath = userKeyPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connection.privateKey) {
|
if (privateKeyPath) {
|
||||||
try {
|
try {
|
||||||
privateKey = (await fs.readFile(connection.privateKey)).toString()
|
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||||
} catch (error) { }
|
} catch (error) {
|
||||||
|
// notify: couldn't read key
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateKey) {
|
||||||
|
this.logger.info('Loaded private key from', privateKeyPath)
|
||||||
|
|
||||||
|
if (privateKey.includes('ENCRYPTED')) {
|
||||||
|
let modal = this.ngbModal.open(PromptModalComponent)
|
||||||
|
modal.componentInstance.prompt = 'Private key passphrase'
|
||||||
|
modal.componentInstance.password = true
|
||||||
|
try {
|
||||||
|
privateKeyPassphrase = await modal.result
|
||||||
|
} catch (_err) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ssh = new Client()
|
let ssh = new Client()
|
||||||
@@ -98,12 +65,12 @@ export class SSHService {
|
|||||||
ssh.on('ready', () => {
|
ssh.on('ready', () => {
|
||||||
connected = true
|
connected = true
|
||||||
if (savedPassword) {
|
if (savedPassword) {
|
||||||
this.savePassword(connection, savedPassword)
|
this.passwordStorage.savePassword(connection, savedPassword)
|
||||||
}
|
}
|
||||||
this.zone.run(resolve)
|
this.zone.run(resolve)
|
||||||
})
|
})
|
||||||
ssh.on('error', error => {
|
ssh.on('error', error => {
|
||||||
this.deletePassword(connection)
|
this.passwordStorage.deletePassword(connection)
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
alert(`SSH error: ${error}`)
|
alert(`SSH error: ${error}`)
|
||||||
@@ -136,6 +103,7 @@ export class SSHService {
|
|||||||
username: connection.user,
|
username: connection.user,
|
||||||
password: privateKey ? undefined : '',
|
password: privateKey ? undefined : '',
|
||||||
privateKey,
|
privateKey,
|
||||||
|
passphrase: privateKeyPassphrase,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
agent,
|
agent,
|
||||||
agentForward: !!agent,
|
agentForward: !!agent,
|
||||||
@@ -148,8 +116,8 @@ export class SSHService {
|
|||||||
return connection.password
|
return connection.password
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keychainPasswordUsed && (wincredmgr || xkeychain.isSupported())) {
|
if (!keychainPasswordUsed) {
|
||||||
let password = await this.loadPassword(connection)
|
let password = await this.passwordStorage.loadPassword(connection)
|
||||||
if (password) {
|
if (password) {
|
||||||
keychainPasswordUsed = true
|
keychainPasswordUsed = true
|
||||||
return password
|
return password
|
||||||
|
@@ -6,6 +6,10 @@
|
|||||||
version "8.0.53"
|
version "8.0.53"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8"
|
||||||
|
|
||||||
|
"@types/openpgp@^0.0.29":
|
||||||
|
version "0.0.29"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/openpgp/-/openpgp-0.0.29.tgz#feabb9d547cb107f7b98fdd51ac616f6cf5aaebd"
|
||||||
|
|
||||||
"@types/ssh2-streams@*":
|
"@types/ssh2-streams@*":
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.2.tgz#7aa18b8c2450f17699e9ea18a76efc838188d58d"
|
resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.2.tgz#7aa18b8c2450f17699e9ea18a76efc838188d58d"
|
||||||
@@ -744,6 +748,12 @@ emojis-list@^2.0.0:
|
|||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||||
|
|
||||||
|
encoding@^0.1.11:
|
||||||
|
version "0.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
|
||||||
|
dependencies:
|
||||||
|
iconv-lite "~0.4.13"
|
||||||
|
|
||||||
enhanced-resolve@3.3.0, enhanced-resolve@^3.3.0:
|
enhanced-resolve@3.3.0, enhanced-resolve@^3.3.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3"
|
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3"
|
||||||
@@ -1029,7 +1039,7 @@ glob@^7.0.5:
|
|||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
|
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
|
||||||
version "4.1.11"
|
version "4.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
||||||
|
|
||||||
@@ -1182,10 +1192,18 @@ https-browserify@^1.0.0:
|
|||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||||
|
|
||||||
|
iconv-lite@~0.4.13:
|
||||||
|
version "0.4.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.4:
|
||||||
version "1.1.8"
|
version "1.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
|
||||||
|
|
||||||
|
imurmurhash@^0.1.4:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||||
|
|
||||||
indent-string@^2.1.0:
|
indent-string@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
|
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
|
||||||
@@ -1369,6 +1387,10 @@ is-regex@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has "^1.0.1"
|
has "^1.0.1"
|
||||||
|
|
||||||
|
is-stream@^1.0.1:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||||
|
|
||||||
is-typedarray@~1.0.0:
|
is-typedarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
@@ -1708,6 +1730,13 @@ nanomatch@^1.2.5:
|
|||||||
snapdragon "^0.8.1"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.1"
|
to-regex "^3.0.1"
|
||||||
|
|
||||||
|
node-fetch@^1.3.3:
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||||
|
dependencies:
|
||||||
|
encoding "^0.1.11"
|
||||||
|
is-stream "^1.0.1"
|
||||||
|
|
||||||
node-libs-browser@^2.0.0:
|
node-libs-browser@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
|
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
|
||||||
@@ -1736,6 +1765,12 @@ node-libs-browser@^2.0.0:
|
|||||||
util "^0.10.3"
|
util "^0.10.3"
|
||||||
vm-browserify "0.0.4"
|
vm-browserify "0.0.4"
|
||||||
|
|
||||||
|
node-localstorage@~1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-localstorage/-/node-localstorage-1.3.0.tgz#2e436aae8dcc9ace97b43c65c16c0d577be0a55c"
|
||||||
|
dependencies:
|
||||||
|
write-file-atomic "^1.1.4"
|
||||||
|
|
||||||
node-pre-gyp@^0.6.39:
|
node-pre-gyp@^0.6.39:
|
||||||
version "0.6.39"
|
version "0.6.39"
|
||||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
|
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
|
||||||
@@ -1844,6 +1879,13 @@ once@^1.3.0, once@^1.3.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
|
openpgp@^2.6.1:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-2.6.1.tgz#7d9da10433e37d87300fbac1fe173c80f0a908c9"
|
||||||
|
dependencies:
|
||||||
|
node-fetch "^1.3.3"
|
||||||
|
node-localstorage "~1.3.0"
|
||||||
|
|
||||||
os-browserify@^0.3.0:
|
os-browserify@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
@@ -2408,6 +2450,10 @@ single-line-log@^1.1.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width "^1.0.1"
|
string-width "^1.0.1"
|
||||||
|
|
||||||
|
slide@^1.1.5:
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
|
||||||
|
|
||||||
snapdragon-node@^2.0.1:
|
snapdragon-node@^2.0.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
|
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
|
||||||
@@ -2901,6 +2947,14 @@ wrappy@1:
|
|||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
|
||||||
|
write-file-atomic@^1.1.4:
|
||||||
|
version "1.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.11"
|
||||||
|
imurmurhash "^0.1.4"
|
||||||
|
slide "^1.1.5"
|
||||||
|
|
||||||
xkeychain@^0.0.6:
|
xkeychain@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/xkeychain/-/xkeychain-0.0.6.tgz#1c58b3dd2f80481f8f67949c3511aa14027c2b9b"
|
resolved "https://registry.yarnpkg.com/xkeychain/-/xkeychain-0.0.6.tgz#1c58b3dd2f80481f8f67949c3511aa14027c2b9b"
|
||||||
|
Reference in New Issue
Block a user