@dotenvx/dotenvx 1.71.3 → 1.72.0

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/CHANGELOG.md CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.71.3...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.72.0...main)
6
+
7
+ ## [1.72.0](https://github.com/dotenvx/dotenvx/compare/v1.71.3...v1.72.0) (2026-06-15)
8
+
9
+ ### Added
10
+
11
+ * Move `armor login` command to first class `dotenvx login` command. ([#836](https://github.com/dotenvx/dotenvx/pull/836))
12
+ * `dotenvx` remains local-first: login is optional and only needed when you want Armor features like moving keys off-device, team sharing, and audit access.
13
+ * Patch 2 high severity dependencies ([#837](https://github.com/dotenvx/dotenvx/pull/837))
6
14
 
7
15
  ## [1.71.3](https://github.com/dotenvx/dotenvx/compare/v1.71.2...v1.71.3) (2026-06-11)
8
16
 
package/README.md CHANGED
@@ -2119,8 +2119,8 @@ Commands:
2119
2119
  run inject env at runtime [dotenvx run -- yourcommand]
2120
2120
  get [KEY] return a single environment variable
2121
2121
  set <KEY> <value> set a single environment variable
2122
- encrypt convert .env file(s) to encrypted .env file(s)
2123
- decrypt convert encrypted .env file(s) to plain .env file(s)
2122
+ encrypt encrypt .env file(s)
2123
+ decrypt decrypt .env file(s)
2124
2124
  keypair [KEY] print public/private keys for .env file(s)
2125
2125
  ls [directory] print all .env files in a tree structure
2126
2126
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.71.3",
2
+ "version": "1.72.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -45,21 +45,26 @@
45
45
  "funding": "https://dotenvx.com",
46
46
  "dependencies": {
47
47
  "commander": "^11.1.0",
48
+ "conf": "^10.2.0",
48
49
  "dotenv": "^17.2.1",
49
50
  "eciesjs": "^0.4.10",
50
51
  "enquirer": "^2.4.1",
52
+ "env-paths": "^2.2.1",
51
53
  "execa": "^5.1.1",
52
54
  "fdir": "^6.2.0",
53
55
  "ignore": "^5.3.0",
54
56
  "object-treeify": "1.1.33",
57
+ "open": "^8.4.2",
55
58
  "picomatch": "^4.0.4",
59
+ "systeminformation": "^5.22.11",
60
+ "undici": "^7.11.0",
56
61
  "which": "^4.0.0",
57
62
  "yocto-spinner": "^1.1.0"
58
63
  },
59
64
  "devDependencies": {
60
- "@yao-pkg/pkg": "^6.14.2",
65
+ "@yao-pkg/pkg": "^6.12.0",
61
66
  "capture-console": "^1.0.2",
62
- "esbuild": "^0.25.8",
67
+ "esbuild": "^0.28.1",
63
68
  "proxyquire": "^2.1.3",
64
69
  "sinon": "^14.0.1",
65
70
  "standard": "^17.1.0",
@@ -13,8 +13,8 @@ async function decrypt () {
13
13
 
14
14
  logger.debug(`options: ${JSON.stringify(options)}`)
15
15
 
16
- const sesh = new Session()
17
16
  const envs = this.envs
17
+ const sesh = new Session()
18
18
  const noArmor = options.armor === false || (await sesh.noArmor())
19
19
 
20
20
  let errorCount = 0
@@ -46,8 +46,8 @@ async function encrypt () {
46
46
 
47
47
  logger.debug(`options: ${JSON.stringify(options)}`)
48
48
 
49
- const sesh = new Session()
50
49
  const envs = this.envs
50
+ const sesh = new Session()
51
51
  const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
52
52
  const noCreate = options.create === false
53
53
 
@@ -0,0 +1,63 @@
1
+ const { logger } = require('../../shared/logger')
2
+
3
+ const Session = require('../../db/session')
4
+ const Login = require('../../lib/services/login')
5
+ const LoginPoll = require('../../lib/services/loginPoll')
6
+
7
+ const createSpinner = require('../../lib/helpers/createSpinner')
8
+ const formatCode = require('../../lib/helpers/formatCode')
9
+ const listenForOpenKey = require('../../lib/helpers/listenForOpenKey')
10
+ const openUrl = require('../../lib/helpers/openUrl')
11
+
12
+ const FRAMES = ['◐', '◓', '◑', '◒']
13
+
14
+ async function login () {
15
+ const options = this.opts()
16
+ const spinner = await createSpinner({ ...options, text: 'logging in', frames: FRAMES })
17
+
18
+ logger.debug(`options: ${JSON.stringify(options)}`)
19
+
20
+ const sesh = new Session()
21
+ await sesh.notifyUpdate()
22
+ const hostname = options.hostname || sesh.hostname()
23
+ let cleanupOpenKeyListener = () => {}
24
+
25
+ try {
26
+ const {
27
+ deviceCode,
28
+ userCode,
29
+ verificationUri,
30
+ verificationUriComplete,
31
+ interval
32
+ } = await new Login(hostname).run()
33
+
34
+ const promptMessage = `◌ press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...`
35
+
36
+ logger.debug(`POST ${hostname} with deviceCode ${deviceCode} at interval ${interval}`)
37
+ logger.info(promptMessage)
38
+
39
+ // begin polling
40
+ const pollPromise = new LoginPoll(hostname, deviceCode, interval).run()
41
+ cleanupOpenKeyListener = listenForOpenKey(() => openUrl(verificationUriComplete))
42
+ const data = await pollPromise
43
+
44
+ cleanupOpenKeyListener()
45
+ if (spinner) spinner.stop()
46
+ logger.success(`◉ logged in (${data.username})`)
47
+ process.exit(0)
48
+ } catch (error) {
49
+ cleanupOpenKeyListener()
50
+ if (spinner) spinner.stop()
51
+ if (error.message) {
52
+ logger.error(error.message)
53
+ } else {
54
+ logger.error(error)
55
+ }
56
+ if (error.stack) {
57
+ logger.debug(error.stack)
58
+ }
59
+ process.exit(1)
60
+ }
61
+ }
62
+
63
+ module.exports = login
@@ -0,0 +1,36 @@
1
+ const { logger } = require('../../shared/logger')
2
+
3
+ const Session = require('../../db/session')
4
+ const Logout = require('../../lib/services/logout')
5
+ const createSpinner = require('../../lib/helpers/createSpinner')
6
+
7
+ const FRAMES = ['◐', '◓', '◑', '◒']
8
+
9
+ async function logout () {
10
+ const options = this.opts()
11
+ const spinner = await createSpinner({ ...options, text: 'logging out', frames: FRAMES })
12
+
13
+ logger.debug(`options: ${JSON.stringify(options)}`)
14
+
15
+ const sesh = new Session()
16
+ const hostname = options.hostname || sesh.hostname()
17
+
18
+ try {
19
+ const data = await new Logout(hostname).run()
20
+ if (spinner) spinner.stop()
21
+ logger.success(`◌ logged out (${data.username})`)
22
+ } catch (error) {
23
+ if (spinner) spinner.stop()
24
+ if (error.message) {
25
+ logger.error(error.message)
26
+ } else {
27
+ logger.error(error)
28
+ }
29
+ if (error.stack) {
30
+ logger.debug(error.stack)
31
+ }
32
+ process.exit(1)
33
+ }
34
+ }
35
+
36
+ module.exports = logout
@@ -1,8 +1,7 @@
1
1
  function normalizeArmorOptions (options) {
2
- if (options.armor === false || options.ops === false || options.vlt === false) {
2
+ if (options.armor === false || options.ops === false) {
3
3
  options.armor = false
4
4
  options.ops = false
5
- options.vlt = false
6
5
  }
7
6
 
8
7
  return options
@@ -24,9 +24,9 @@ async function set (key, value) {
24
24
  logger.debug(`options: ${JSON.stringify(options)}`)
25
25
 
26
26
  try {
27
- const sesh = new Session()
28
27
  const envs = this.envs
29
28
  const envKeysFilepath = options.envKeysFile
29
+ const sesh = new Session()
30
30
  const noArmor = options.armor === false || (await sesh.noArmor())
31
31
  const noCreate = options.create === false
32
32
 
@@ -72,7 +72,6 @@ program.command('run')
72
72
  .option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
73
73
  .option('--token <token>', 'set Armor ⛨ token')
74
74
  .option('--no-armor', 'disable dotenvx-armor features')
75
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
76
75
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
77
76
  .action(function (...args) {
78
77
  this.envs = envs
@@ -96,7 +95,6 @@ program.command('get')
96
95
  .option('--pp', 'pretty print output (alias)')
97
96
  .option('--format <type>', 'format of the output (json, shell, colon, eval)', 'json')
98
97
  .option('--no-armor', 'disable dotenvx-armor features')
99
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
100
98
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
101
99
  .action(function (...args) {
102
100
  this.envs = envs
@@ -117,7 +115,6 @@ program.command('set')
117
115
  .option('-p, --plain', 'store value as plain text', false)
118
116
  .option('--no-create', 'do not create .env file(s) when missing')
119
117
  .option('--no-armor', 'disable dotenvx-armor features')
120
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
121
118
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
122
119
  .action(function (...args) {
123
120
  this.envs = envs
@@ -126,7 +123,7 @@ program.command('set')
126
123
 
127
124
  // dotenvx encrypt
128
125
  program.command('encrypt')
129
- .description('convert .env file(s) to encrypted .env file(s)')
126
+ .description('encrypt .env file(s)')
130
127
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
131
128
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
132
129
  .option('-k, --key <keys...>', 'keys(s) to encrypt (default: all keys in file)')
@@ -135,7 +132,6 @@ program.command('encrypt')
135
132
  .option('--token <token>', 'set Armor ⛨ token')
136
133
  .option('--no-create', 'do not create .env file(s) when missing')
137
134
  .option('--no-armor', 'disable dotenvx-armor features')
138
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
139
135
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
140
136
  .action(function (...args) {
141
137
  this.envs = envs
@@ -144,13 +140,12 @@ program.command('encrypt')
144
140
 
145
141
  // dotenvx decrypt
146
142
  program.command('decrypt')
147
- .description('convert encrypted .env file(s) to plain .env file(s)')
143
+ .description('decrypt .env file(s)')
148
144
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
149
145
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
150
146
  .option('-k, --key <keys...>', 'keys(s) to decrypt (default: all keys in file)')
151
147
  .option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from decryption (default: none)')
152
148
  .option('--no-armor', 'disable dotenvx-armor features')
153
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
154
149
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
155
150
  .option('--stdout', 'send to stdout')
156
151
  .action(function (...args) {
@@ -166,7 +161,6 @@ program.command('rotate')
166
161
  .option('-k, --key <keys...>', 'keys(s) to encrypt (default: all keys in file)')
167
162
  .option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from encryption (default: none)')
168
163
  .option('--no-armor', 'disable dotenvx-armor features')
169
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
170
164
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
171
165
  .option('--stdout', 'send to stdout')
172
166
  .action(function (...args) {
@@ -182,7 +176,6 @@ program.command('keypair')
182
176
  .option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
183
177
  .option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
184
178
  .option('--no-armor', 'disable dotenvx-armor features')
185
- .addOption(new Option('--no-vlt', 'disable dotenvx-vlt features (deprecated. use --no-armor)').hideHelp())
186
179
  .addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
187
180
  .option('-pp, --pretty-print', 'pretty print output')
188
181
  .option('--pp', 'pretty print output (alias)')
@@ -211,20 +204,20 @@ program.command('doctor', { hidden: true })
211
204
 
212
205
  // dotenvx login
213
206
  program.command('login', { hidden: true })
214
- .description('')
207
+ .description('log in to move keys off-device, share with your team, and audit access')
215
208
  .allowUnknownOption()
216
- .action(() => {
217
- const rawArgs = ['armor', 'login', ...process.argv.slice(3)]
218
- executeDynamic(program, 'armor', rawArgs)
209
+ .option('--hostname <hostname>', 'set Armor ⛨ hostname')
210
+ .action(function (...args) {
211
+ return require('./actions/login').apply(this, args)
219
212
  })
220
213
 
221
214
  // dotenvx logout
222
215
  program.command('logout', { hidden: true })
223
- .description('log out of your dotenvx account')
216
+ .description('log out of connected security features')
224
217
  .allowUnknownOption()
225
- .action(() => {
226
- const rawArgs = ['armor', 'logout', ...process.argv.slice(3)]
227
- executeDynamic(program, 'armor', rawArgs)
218
+ .option('--hostname <hostname>', 'set Armor ⛨ hostname')
219
+ .action(function (...args) {
220
+ return require('./actions/logout').apply(this, args)
228
221
  })
229
222
 
230
223
  // dotenvx help
@@ -245,8 +238,10 @@ program.command('help [command]')
245
238
 
246
239
  // dotenvx armor
247
240
  program.addHelpText('after', ' ')
248
- program.addHelpText('after', 'Advanced: ')
249
- program.addHelpText('after', ' armor ARMORED KEYS [www.dotenvx.com/armor]')
241
+ program.addHelpText('after', 'Professional Security: ')
242
+ program.addHelpText('after', ' login log in to move keys off-device, share with your team, and audit access')
243
+ program.addHelpText('after', ' logout log out of connected security features')
244
+ program.addHelpText('after', ' armor ⛨ move private keys off-device [www.dotenvx.com/armor]')
250
245
  program.addHelpText('after', ' ext ⊕ extensions')
251
246
 
252
247
  // dotenvx ext
@@ -0,0 +1,73 @@
1
+ const Conf = require('conf')
2
+ const { PrivateKey } = require('eciesjs')
3
+
4
+ const encryptDeviceValue = require('./../lib/helpers/encryptDeviceValue')
5
+ const decryptDeviceValue = require('./../lib/helpers/decryptDeviceValue')
6
+
7
+ class Device {
8
+ constructor () {
9
+ this.store = new Conf({
10
+ cwd: process.env.DOTENVX_CONFIG || undefined,
11
+ projectName: 'dotenvx',
12
+ configName: '.device-key',
13
+ projectSuffix: '',
14
+ fileExtension: '',
15
+ encryptionKey: 'dotenvxpro dotenvxpro dotenvxpro' // backwards compatible
16
+ })
17
+ }
18
+
19
+ touch () {
20
+ const _privateKey = this.privateKey()
21
+ const _publicKey = this.publicKey()
22
+
23
+ return {
24
+ privateKey: _privateKey,
25
+ publicKey: _publicKey
26
+ }
27
+ }
28
+
29
+ configPath () {
30
+ return this.store.path
31
+ }
32
+
33
+ privateKey () {
34
+ const currentPrivateKey = this.store.get('private_key/1')
35
+ if (currentPrivateKey && currentPrivateKey.length > 0) {
36
+ this.store.set('private_key/1', currentPrivateKey)
37
+
38
+ return currentPrivateKey
39
+ }
40
+
41
+ // generate privateKey for the first time
42
+ const kp = new PrivateKey()
43
+ const _privateKey = kp.secret.toString('hex')
44
+
45
+ this.store.set('private_key/1', _privateKey)
46
+
47
+ return _privateKey
48
+ }
49
+
50
+ publicKey () {
51
+ // must have private key to try and get public key
52
+ const privateKeyHex = this.privateKey()
53
+ if (!privateKeyHex || privateKeyHex.length < 1) {
54
+ return ''
55
+ }
56
+
57
+ // create keyPair object from hex string
58
+ const _privateKey = new PrivateKey(Buffer.from(privateKeyHex, 'hex'))
59
+
60
+ // compute publicKey from privateKey
61
+ return _privateKey.publicKey.toHex()
62
+ }
63
+
64
+ encrypt (value) {
65
+ return encryptDeviceValue(value, this.publicKey())
66
+ }
67
+
68
+ decrypt (value) {
69
+ return decryptDeviceValue(value, this.privateKey())
70
+ }
71
+ }
72
+
73
+ module.exports = Device
package/src/db/session.js CHANGED
@@ -1,9 +1,139 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const Conf = require('conf')
4
+ const dotenv = require('dotenv')
5
+ const envPaths = require('env-paths')
6
+
1
7
  const Armor = require('./../lib/extensions/armor')
8
+ const jsonToEnv = require('./../lib/helpers/jsonToEnv')
2
9
  const { logger } = require('./../shared/logger')
3
10
 
11
+ const ARMOR = {
12
+ HOSTNAME: 'DOTENVX_ARMOR_HOSTNAME',
13
+ USER: 'DOTENVX_ARMOR_USER',
14
+ USERNAME: 'DOTENVX_ARMOR_USERNAME',
15
+ TOKEN: 'DOTENVX_ARMOR_TOKEN',
16
+ ON: 'DOTENVX_ARMOR_ON',
17
+ VERSION: 'DOTENVX_ARMOR_VERSION',
18
+ VERSION_LAST_CHECK: 'DOTENVX_ARMOR_VERSION_LAST_CHECK'
19
+ }
20
+
4
21
  class Session {
5
22
  constructor () {
6
23
  this.armor = new Armor()
24
+ this._store = null
25
+ }
26
+
27
+ _configPath () {
28
+ const cwd = process.env.DOTENVX_CONFIG || this._defaultConfigCwd()
29
+ return path.resolve(cwd, '.env')
30
+ }
31
+
32
+ _defaultConfigCwd () {
33
+ return envPaths('dotenvx', { suffix: '' }).config
34
+ }
35
+
36
+ _configExists () {
37
+ return fs.existsSync(this._configPath())
38
+ }
39
+
40
+ _newStore () {
41
+ return new Conf({
42
+ cwd: process.env.DOTENVX_CONFIG || undefined,
43
+ projectName: 'dotenvx',
44
+ configName: '.env',
45
+ projectSuffix: '',
46
+ fileExtension: '',
47
+ serialize: function (json) {
48
+ return jsonToEnv(json)
49
+ },
50
+ // Convert .env format to an object
51
+ deserialize: function (env) {
52
+ return dotenv.parse(env)
53
+ }
54
+ })
55
+ }
56
+
57
+ createStore () {
58
+ if (!this._store) this._store = this._newStore()
59
+ return this._store
60
+ }
61
+
62
+ openStore () {
63
+ if (!this._store && !this._configExists()) {
64
+ return null
65
+ }
66
+
67
+ if (!this._store) this._store = this._newStore()
68
+ return this._store
69
+ }
70
+
71
+ get store () {
72
+ return this.openStore()
73
+ }
74
+
75
+ status () {
76
+ // if logged in
77
+ if (this.username() && this.token() && this.on()) {
78
+ return 'on'
79
+ }
80
+
81
+ return 'off'
82
+ }
83
+
84
+ //
85
+ // Get
86
+ //
87
+ readSetting (key) {
88
+ const store = this.openStore()
89
+ if (!store) return undefined
90
+
91
+ return store.get(ARMOR[key])
92
+ }
93
+
94
+ hostname () {
95
+ return this.readSetting('HOSTNAME') || 'https://armor.dotenvx.com'
96
+ }
97
+
98
+ username () {
99
+ return this.readSetting('USERNAME') || undefined
100
+ }
101
+
102
+ token () {
103
+ return this.readSetting('TOKEN') || undefined
104
+ }
105
+
106
+ devicePublicKey () {
107
+ const Device = require('./device')
108
+ return new Device().publicKey()
109
+ }
110
+
111
+ path () {
112
+ return this._store ? this._store.path : this._configPath()
113
+ }
114
+
115
+ on () {
116
+ return (this.readSetting('ON') || 'true') === 'true'
117
+ }
118
+
119
+ off () {
120
+ return (this.readSetting('ON') || 'true') === 'false'
121
+ }
122
+
123
+ async systemInformation () {
124
+ const si = require('systeminformation')
125
+ const system = await si.system()
126
+ const osInfo = await si.osInfo()
127
+
128
+ return {
129
+ system_uuid: system.uuid,
130
+ os_platform: osInfo.platform,
131
+ os_arch: osInfo.arch
132
+ }
133
+ }
134
+
135
+ async notifyUpdate () {
136
+ // native login keeps this lightweight; sidecar commands still handle full update messaging
7
137
  }
8
138
 
9
139
  //
@@ -20,6 +150,62 @@ class Session {
20
150
  logger.debug(`armor: ${status}`)
21
151
  return status === 'off'
22
152
  }
153
+
154
+ //
155
+ // Set/Delete
156
+ //
157
+ login (hostname, id, username, accessToken) {
158
+ if (!hostname) {
159
+ throw new Error('DOTENVX_ARMOR_HOSTNAME not set. Run [dotenvx login]')
160
+ }
161
+
162
+ if (!id) {
163
+ throw new Error('DOTENVX_ARMOR_USER not set. Run [dotenvx login]')
164
+ }
165
+
166
+ if (!username) {
167
+ throw new Error('DOTENVX_ARMOR_USERNAME not set. Run [dotenvx login]')
168
+ }
169
+
170
+ if (!accessToken) {
171
+ throw new Error('DOTENVX_ARMOR_TOKEN not set. Run [dotenvx login]')
172
+ }
173
+
174
+ const store = this.createStore()
175
+ store.set(ARMOR.USER, id)
176
+ store.set(ARMOR.USERNAME, username)
177
+ store.set(ARMOR.TOKEN, accessToken)
178
+ store.set(ARMOR.HOSTNAME, hostname)
179
+ store.set(ARMOR.ON, 'true')
180
+
181
+ return accessToken
182
+ }
183
+
184
+ logout (hostname, id, accessToken) {
185
+ if (!hostname) {
186
+ throw new Error('DOTENVX_ARMOR_HOSTNAME not set. Run [dotenvx login]')
187
+ }
188
+
189
+ if (!id) {
190
+ throw new Error('DOTENVX_ARMOR_USER not set. Run [dotenvx login]')
191
+ }
192
+
193
+ if (!accessToken) {
194
+ throw new Error('DOTENVX_ARMOR_TOKEN not set. Run [dotenvx login]')
195
+ }
196
+
197
+ const store = this.openStore()
198
+ if (!store) return true
199
+
200
+ store.delete(ARMOR.USER)
201
+ store.delete(ARMOR.USERNAME)
202
+ store.delete(ARMOR.TOKEN)
203
+ store.delete(ARMOR.HOSTNAME)
204
+ store.delete(ARMOR.ON)
205
+ store.delete(ARMOR.VERSION)
206
+ store.delete(ARMOR.VERSION_LAST_CHECK)
207
+ return true
208
+ }
23
209
  }
24
210
 
25
211
  module.exports = Session
@@ -0,0 +1,34 @@
1
+ const { http } = require('../helpers/http')
2
+ const buildApiError = require('../helpers/buildApiError')
3
+ const normalizeToken = require('../helpers/normalizeToken')
4
+
5
+ class PostLogout {
6
+ constructor (hostname, token) {
7
+ this.hostname = hostname
8
+ this.token = token
9
+ }
10
+
11
+ async run () {
12
+ const token = normalizeToken(this.token)
13
+ const url = `${this.hostname}/api/logout`
14
+
15
+ const resp = await http(url, {
16
+ method: 'POST',
17
+ headers: {
18
+ Authorization: `Bearer ${token}`,
19
+ 'Content-Type': 'application/json'
20
+ },
21
+ body: JSON.stringify({})
22
+ })
23
+
24
+ const json = await resp.body.json()
25
+
26
+ if (resp.statusCode >= 400) {
27
+ throw buildApiError(resp.statusCode, json)
28
+ }
29
+
30
+ return json
31
+ }
32
+ }
33
+
34
+ module.exports = PostLogout
@@ -0,0 +1,38 @@
1
+ const { http } = require('../helpers/http')
2
+ const buildOauthError = require('../helpers/buildOauthError')
3
+
4
+ const OAUTH_CLIENT_ID = 'oac_dotenvxcli'
5
+
6
+ class PostOauthDeviceCode {
7
+ constructor (hostname, devicePublicKey, systemInformation, dotenvxProjectId = null) {
8
+ this.hostname = hostname
9
+ this.devicePublicKey = devicePublicKey
10
+ this.systemInformation = systemInformation
11
+ this.dotenvxProjectId = dotenvxProjectId
12
+ }
13
+
14
+ async run () {
15
+ const resp = await http(`${this.hostname}/oauth/device/code`, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify({
21
+ client_id: OAUTH_CLIENT_ID,
22
+ device_public_key: this.devicePublicKey,
23
+ system_information: this.systemInformation,
24
+ dotenvx_project_id: this.dotenvxProjectId
25
+ })
26
+ })
27
+
28
+ const json = await resp.body.json()
29
+
30
+ if (resp.statusCode >= 400) {
31
+ throw buildOauthError(resp.statusCode, json)
32
+ }
33
+
34
+ return json
35
+ }
36
+ }
37
+
38
+ module.exports = PostOauthDeviceCode
@@ -0,0 +1,35 @@
1
+ const { http } = require('../helpers/http')
2
+ const buildOauthError = require('../helpers/buildOauthError')
3
+
4
+ const OAUTH_CLIENT_ID = 'oac_dotenvxcli'
5
+
6
+ class PostOauthToken {
7
+ constructor (hostname, deviceCode) {
8
+ this.hostname = hostname
9
+ this.deviceCode = deviceCode
10
+ }
11
+
12
+ async run () {
13
+ const resp = await http(`${this.hostname}/oauth/token`, {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json'
17
+ },
18
+ body: JSON.stringify({
19
+ client_id: OAUTH_CLIENT_ID,
20
+ device_code: this.deviceCode,
21
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
22
+ })
23
+ })
24
+
25
+ const json = await resp.body.json()
26
+
27
+ if (resp.statusCode >= 400) {
28
+ throw buildOauthError(resp.statusCode, json)
29
+ }
30
+
31
+ return json
32
+ }
33
+ }
34
+
35
+ module.exports = PostOauthToken
@@ -0,0 +1,16 @@
1
+ function buildApiError (statusCode, json) {
2
+ const code = json.error.code || statusCode.toString()
3
+ const message = `[${code}] ${json.error.message}`
4
+ const help = `[${code}] ${json.error.help || JSON.stringify(json)}`
5
+ const meta = json.error.meta
6
+
7
+ const error = new Error(message)
8
+ error.code = code
9
+ error.help = help
10
+ error.meta = meta
11
+ error.json = json
12
+
13
+ return error
14
+ }
15
+
16
+ module.exports = buildApiError
@@ -0,0 +1,14 @@
1
+ function buildOauthError (statusCode, json) {
2
+ const code = json.error
3
+ const message = `[${code}] ${json.error_description}`
4
+ const help = `[${code}] ${JSON.stringify(json)}`
5
+
6
+ const error = new Error(message)
7
+ error.code = code
8
+ error.help = help
9
+ error.statusCode = statusCode
10
+
11
+ return error
12
+ }
13
+
14
+ module.exports = buildOauthError
@@ -4,7 +4,7 @@ const FRAME_INTERVAL_MS = 80
4
4
  async function createSpinner (options = {}) {
5
5
  const stream = process.stderr
6
6
  const hasCursorControls = typeof stream.cursorTo === 'function' && typeof stream.clearLine === 'function'
7
- const enabled = Boolean(stream.isTTY && hasCursorControls && !options.quiet && !options.verbose && !options.debug)
7
+ const enabled = Boolean(stream.isTTY && hasCursorControls && options.spinner !== false && !options.quiet && !options.verbose && !options.debug)
8
8
  if (!enabled) return null
9
9
 
10
10
  const text = options.text || 'thinking'
@@ -0,0 +1,10 @@
1
+ const { decrypt } = require('eciesjs')
2
+
3
+ function decryptDeviceValue (value, privateKey) {
4
+ const secret = Buffer.from(privateKey, 'hex')
5
+ const ciphertext = Buffer.from(value, 'base64')
6
+
7
+ return decrypt(secret, ciphertext).toString()
8
+ }
9
+
10
+ module.exports = decryptDeviceValue
@@ -0,0 +1,9 @@
1
+ const { encrypt } = require('eciesjs')
2
+
3
+ function encryptDeviceValue (value, publicKey) {
4
+ const ciphertext = encrypt(publicKey, Buffer.from(value))
5
+
6
+ return Buffer.from(ciphertext, 'hex').toString('base64')
7
+ }
8
+
9
+ module.exports = encryptDeviceValue
@@ -0,0 +1,11 @@
1
+ function formatCode (str) {
2
+ const parts = []
3
+
4
+ for (let i = 0; i < str.length; i += 4) {
5
+ parts.push(str.substring(i, i + 4))
6
+ }
7
+
8
+ return parts.join('-')
9
+ }
10
+
11
+ module.exports = formatCode
@@ -0,0 +1,7 @@
1
+ const { request } = require('undici')
2
+
3
+ async function http (url, opts = {}) {
4
+ return await request(url, opts)
5
+ }
6
+
7
+ module.exports = { http }
@@ -0,0 +1,7 @@
1
+ function jsonToEnv (json) {
2
+ return Object.entries(json).map(function ([key, value]) {
3
+ return key + '=' + `"${value}"`
4
+ }).join('\n')
5
+ }
6
+
7
+ module.exports = jsonToEnv
@@ -0,0 +1,46 @@
1
+ function listenForOpenKey (onOpen) {
2
+ const stdin = process.stdin
3
+ if (!stdin.isTTY) return () => {}
4
+
5
+ const canSetRawMode = typeof stdin.setRawMode === 'function'
6
+ const wasRawMode = Boolean(stdin.isRaw)
7
+ let didHandleOpenChoice = false
8
+
9
+ const cleanup = () => {
10
+ stdin.off('data', onData)
11
+ if (canSetRawMode) stdin.setRawMode(wasRawMode)
12
+ stdin.pause()
13
+ }
14
+
15
+ const onData = (chunk) => {
16
+ const key = String(chunk)
17
+ const lower = key.toLowerCase()
18
+
19
+ if (key === '\u0003') {
20
+ cleanup()
21
+ process.kill(process.pid, 'SIGINT')
22
+ return
23
+ }
24
+
25
+ if (key === '\r' || key === '\n' || lower === 'y') {
26
+ if (!didHandleOpenChoice) {
27
+ didHandleOpenChoice = true
28
+ Promise.resolve(onOpen()).catch(() => {})
29
+ }
30
+ return
31
+ }
32
+
33
+ if (lower === 'n') {
34
+ cleanup()
35
+ process.kill(process.pid, 'SIGINT')
36
+ }
37
+ }
38
+
39
+ if (canSetRawMode) stdin.setRawMode(true)
40
+ stdin.resume()
41
+ stdin.on('data', onData)
42
+
43
+ return cleanup
44
+ }
45
+
46
+ module.exports = listenForOpenKey
@@ -0,0 +1,5 @@
1
+ function normalizeToken (token) {
2
+ return token == null ? '' : token
3
+ }
4
+
5
+ module.exports = normalizeToken
@@ -0,0 +1,7 @@
1
+ const open = require('open')
2
+
3
+ async function openUrl (url) {
4
+ return await open(url, { wait: false })
5
+ }
6
+
7
+ module.exports = openUrl
package/src/lib/main.d.ts CHANGED
@@ -179,14 +179,6 @@ export interface DotenvConfigOptions {
179
179
  */
180
180
  noArmor?: boolean;
181
181
 
182
- /**
183
- * Turn off Dotenvx Armor features. Alias for `noArmor`.
184
- *
185
- * @default false
186
- * @example require('@dotenvx/dotenvx').config({ noVlt: true })
187
- */
188
- noVlt?: boolean;
189
-
190
182
  /**
191
183
  * Turn off Dotenvx Armor features. Alias for `noArmor`.
192
184
  *
@@ -270,14 +262,6 @@ export interface SetOptions {
270
262
  */
271
263
  noArmor?: boolean;
272
264
 
273
- /**
274
- * Turn off Dotenvx Armor features. Alias for `noArmor`.
275
- *
276
- * @default false
277
- * @example require('@dotenvx/dotenvx').set(key, value, { noVlt: true })
278
- */
279
- noVlt?: boolean;
280
-
281
265
  /**
282
266
  * Turn off Dotenvx Armor features. Alias for `noArmor`.
283
267
  *
@@ -363,14 +347,6 @@ export interface GetOptions {
363
347
  */
364
348
  noArmor?: boolean;
365
349
 
366
- /**
367
- * Turn off Dotenvx Armor features. Alias for `noArmor`.
368
- *
369
- * @default false
370
- * @example require('@dotenvx/dotenvx').get('KEY', { noVlt: true })
371
- */
372
- noVlt?: boolean;
373
-
374
350
  /**
375
351
  * Turn off Dotenvx Armor features. Alias for `noArmor`.
376
352
  *
package/src/lib/main.js CHANGED
@@ -325,7 +325,7 @@ const keypair = function (envFile, key, envKeysFile = null, noArmor = false) {
325
325
 
326
326
  function resolveNoArmor (options = {}) {
327
327
  const sesh = new Session()
328
- return options.noArmor === true || options.noVlt === true || options.noOps === true || (!options.token && sesh.noArmorSync())
328
+ return options.noArmor === true || options.noOps === true || (!options.token && sesh.noArmorSync())
329
329
  }
330
330
 
331
331
  module.exports = {
@@ -0,0 +1,26 @@
1
+ const Session = require('../../db/session')
2
+ const PostOauthDeviceCode = require('../api/postOauthDeviceCode')
3
+
4
+ class Login {
5
+ constructor (hostname) {
6
+ this.hostname = hostname
7
+ }
8
+
9
+ async run () {
10
+ const sesh = new Session()
11
+ const devicePublicKey = sesh.devicePublicKey()
12
+ const systemInformation = await sesh.systemInformation()
13
+
14
+ const data = await new PostOauthDeviceCode(this.hostname, devicePublicKey, systemInformation).run()
15
+
16
+ return {
17
+ deviceCode: data.device_code,
18
+ userCode: data.user_code,
19
+ verificationUri: data.verification_uri,
20
+ verificationUriComplete: data.verification_uri_complete,
21
+ interval: data.interval
22
+ }
23
+ }
24
+ }
25
+
26
+ module.exports = Login
@@ -0,0 +1,35 @@
1
+ const Session = require('../../db/session')
2
+ const PostOauthToken = require('../api/postOauthToken')
3
+
4
+ class LoginPoll {
5
+ constructor (hostname, deviceCode, interval) {
6
+ this.hostname = hostname
7
+ this.deviceCode = deviceCode
8
+ this.interval = interval
9
+ }
10
+
11
+ async run () {
12
+ const sesh = new Session()
13
+
14
+ while (true) {
15
+ try {
16
+ const data = await new PostOauthToken(this.hostname, this.deviceCode).run()
17
+
18
+ if (data.access_token) {
19
+ sesh.login(this.hostname, data.id, data.username, data.access_token)
20
+ return data
21
+ }
22
+
23
+ await new Promise(resolve => setTimeout(resolve, this.interval * 1000))
24
+ } catch (error) {
25
+ if (error.code === 'authorization_pending') {
26
+ await new Promise(resolve => setTimeout(resolve, (this.interval + 1) * 1000))
27
+ } else {
28
+ throw error
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ module.exports = LoginPoll
@@ -0,0 +1,26 @@
1
+ const Session = require('../../db/session')
2
+ const PostLogout = require('../api/postLogout')
3
+
4
+ class Logout {
5
+ constructor (hostname) {
6
+ this.hostname = hostname
7
+ }
8
+
9
+ async run () {
10
+ const sesh = new Session()
11
+ const token = sesh.token()
12
+
13
+ const data = await new PostLogout(this.hostname, token).run()
14
+
15
+ const id = data.id
16
+ const username = data.username
17
+ const accessToken = data.access_token
18
+ const settingsDevicesUrl = `${this.hostname}/settings/devices`
19
+
20
+ sesh.logout(this.hostname, id, accessToken)
21
+
22
+ return { username, accessToken, settingsDevicesUrl }
23
+ }
24
+ }
25
+
26
+ module.exports = Logout