@dotenvx/dotenvx-ops 0.39.1 → 0.40.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 +7 -1
- package/package.json +1 -1
- package/src/cli/actions/armor/pull.js +35 -0
- package/src/cli/actions/armor/push.js +35 -0
- package/src/cli/commands/armor.js +17 -0
- package/src/lib/api/postArmorPull.js +45 -0
- package/src/lib/api/postArmorPush.js +45 -0
- package/src/lib/helpers/canonicalEnvFilename.js +13 -0
- package/src/lib/helpers/keyNamesForEnvFile.js +43 -0
- package/src/lib/helpers/upsertEnvKey.js +61 -0
- package/src/lib/services/armorPull.js +39 -0
- package/src/lib/services/armorPush.js +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
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-ops/compare/v0.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx-ops/compare/v0.40.0...main)
|
|
6
|
+
|
|
7
|
+
## [0.40.0](https://github.com/dotenvx/dotenvx-ops/compare/v0.39.1...v0.40.0) (2026-04-24)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add `armor push` and `armor pull` ([#56](https://github.com/dotenvx/dotenvx-ops/pull/56))
|
|
6
12
|
|
|
7
13
|
## [0.39.1](https://github.com/dotenvx/dotenvx-ops/compare/v0.39.0...v0.39.1) (2026-04-23)
|
|
8
14
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { logger } = require('@dotenvx/dotenvx')
|
|
2
|
+
const Session = require('./../../../db/session')
|
|
3
|
+
const ArmorPull = require('./../../../lib/services/armorPull')
|
|
4
|
+
const createSpinner = require('../../../lib/helpers/createSpinner2')
|
|
5
|
+
|
|
6
|
+
async function pull () {
|
|
7
|
+
const options = this.opts()
|
|
8
|
+
const spinner = await createSpinner({ ...options, text: 'pulling' })
|
|
9
|
+
|
|
10
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
11
|
+
|
|
12
|
+
const sesh = new Session()
|
|
13
|
+
await sesh.notifyUpdate()
|
|
14
|
+
const hostname = options.hostname || sesh.hostname()
|
|
15
|
+
const token = options.token || sesh.token()
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const devicePublicKey = sesh.devicePublicKey()
|
|
19
|
+
|
|
20
|
+
const { changed, privateKeyName } = await new ArmorPull(hostname, token, devicePublicKey, options.envFile).run()
|
|
21
|
+
|
|
22
|
+
if (spinner) spinner.stop()
|
|
23
|
+
if (changed) {
|
|
24
|
+
logger.success(`◇ pulled ${privateKeyName} (.env.keys)`)
|
|
25
|
+
} else {
|
|
26
|
+
logger.info(`○ no change ${privateKeyName} (.env.keys)`)
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (spinner) spinner.stop()
|
|
30
|
+
logger.error(error.message)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = pull
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { logger } = require('@dotenvx/dotenvx')
|
|
2
|
+
const Session = require('./../../../db/session')
|
|
3
|
+
const ArmorPush = require('./../../../lib/services/armorPush')
|
|
4
|
+
const createSpinner = require('../../../lib/helpers/createSpinner2')
|
|
5
|
+
|
|
6
|
+
async function push () {
|
|
7
|
+
const options = this.opts()
|
|
8
|
+
const spinner = await createSpinner({ ...options, text: 'pushing' })
|
|
9
|
+
|
|
10
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
11
|
+
|
|
12
|
+
const sesh = new Session()
|
|
13
|
+
await sesh.notifyUpdate()
|
|
14
|
+
const hostname = options.hostname || sesh.hostname()
|
|
15
|
+
const token = options.token || sesh.token()
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const devicePublicKey = sesh.devicePublicKey()
|
|
19
|
+
|
|
20
|
+
const { changed, privateKeyName } = await new ArmorPush(hostname, token, devicePublicKey, options.envFile).run()
|
|
21
|
+
|
|
22
|
+
if (spinner) spinner.stop()
|
|
23
|
+
if (changed) {
|
|
24
|
+
logger.success(`◈ pushed ${privateKeyName} (armored ⛨)`)
|
|
25
|
+
} else {
|
|
26
|
+
logger.info(`○ no change ${privateKeyName} (armored ⛨)`)
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (spinner) spinner.stop()
|
|
30
|
+
logger.error(error.message)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = push
|
|
@@ -19,10 +19,27 @@ armor
|
|
|
19
19
|
.description('harden private key')
|
|
20
20
|
.action(upAction)
|
|
21
21
|
|
|
22
|
+
// dotenvx-ops armor down
|
|
22
23
|
const downAction = require('./../actions/armor/down')
|
|
23
24
|
armor
|
|
24
25
|
.command('down')
|
|
25
26
|
.description('soften private key')
|
|
26
27
|
.action(downAction)
|
|
27
28
|
|
|
29
|
+
// dotenvx-ops armor push
|
|
30
|
+
const pushAction = require('./../actions/armor/push')
|
|
31
|
+
armor
|
|
32
|
+
.command('push')
|
|
33
|
+
.description('push armored key (from .env.keys)')
|
|
34
|
+
.option('-f, --env-file <path>', 'path to your env file')
|
|
35
|
+
.action(pushAction)
|
|
36
|
+
|
|
37
|
+
// dotenvx-ops armor pull
|
|
38
|
+
const pullAction = require('./../actions/armor/pull')
|
|
39
|
+
armor
|
|
40
|
+
.command('pull')
|
|
41
|
+
.description('pull armored key (into .env.keys)')
|
|
42
|
+
.option('-f, --env-file <path>', 'path to your env file')
|
|
43
|
+
.action(pullAction)
|
|
44
|
+
|
|
28
45
|
module.exports = armor
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { http } = require('../../lib/helpers/http')
|
|
2
|
+
const buildApiError = require('../../lib/helpers/buildApiError')
|
|
3
|
+
const packageJson = require('../../lib/helpers/packageJson')
|
|
4
|
+
const normalizeToken = require('../../lib/helpers/normalizeToken')
|
|
5
|
+
|
|
6
|
+
class PostArmorPull {
|
|
7
|
+
constructor (hostname, token, devicePublicKey, publicKey) {
|
|
8
|
+
this.hostname = hostname || 'https://ops.dotenvx.com'
|
|
9
|
+
this.token = token
|
|
10
|
+
this.devicePublicKey = devicePublicKey
|
|
11
|
+
this.publicKey = publicKey
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run () {
|
|
15
|
+
const token = normalizeToken(this.token)
|
|
16
|
+
const devicePublicKey = this.devicePublicKey
|
|
17
|
+
const publicKey = this.publicKey
|
|
18
|
+
const url = `${this.hostname}/api/armor/pull`
|
|
19
|
+
|
|
20
|
+
const body = {
|
|
21
|
+
device_public_key: devicePublicKey,
|
|
22
|
+
cli_version: packageJson.version,
|
|
23
|
+
public_key: publicKey
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resp = await http(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const json = await resp.body.json()
|
|
36
|
+
|
|
37
|
+
if (resp.statusCode >= 400) {
|
|
38
|
+
throw buildApiError(resp.statusCode, json)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return json
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = PostArmorPull
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { http } = require('../../lib/helpers/http')
|
|
2
|
+
const buildApiError = require('../../lib/helpers/buildApiError')
|
|
3
|
+
const packageJson = require('../../lib/helpers/packageJson')
|
|
4
|
+
const normalizeToken = require('../../lib/helpers/normalizeToken')
|
|
5
|
+
|
|
6
|
+
class PostArmorPush {
|
|
7
|
+
constructor (hostname, token, devicePublicKey, privateKey) {
|
|
8
|
+
this.hostname = hostname || 'https://ops.dotenvx.com'
|
|
9
|
+
this.token = token
|
|
10
|
+
this.devicePublicKey = devicePublicKey
|
|
11
|
+
this.privateKey = privateKey
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run () {
|
|
15
|
+
const token = normalizeToken(this.token)
|
|
16
|
+
const devicePublicKey = this.devicePublicKey
|
|
17
|
+
const privateKey = this.privateKey
|
|
18
|
+
const url = `${this.hostname}/api/armor/push`
|
|
19
|
+
|
|
20
|
+
const body = {
|
|
21
|
+
device_public_key: devicePublicKey,
|
|
22
|
+
cli_version: packageJson.version,
|
|
23
|
+
private_key: privateKey
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resp = await http(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const json = await resp.body.json()
|
|
36
|
+
|
|
37
|
+
if (resp.statusCode >= 400) {
|
|
38
|
+
throw buildApiError(resp.statusCode, json)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return json
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = PostArmorPush
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
|
|
3
|
+
function canonicalEnvFilename (filepath) {
|
|
4
|
+
let filename = path.basename(filepath).toLowerCase()
|
|
5
|
+
|
|
6
|
+
if (filename.startsWith('.env') && filename.endsWith('.txt')) {
|
|
7
|
+
filename = filename.slice(0, -4)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return filename
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = canonicalEnvFilename
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const canonicalEnvFilename = require('./canonicalEnvFilename')
|
|
2
|
+
|
|
3
|
+
function environment (filepath) {
|
|
4
|
+
const filename = canonicalEnvFilename(filepath)
|
|
5
|
+
|
|
6
|
+
const parts = filename.split('.')
|
|
7
|
+
const possibleEnvironmentList = [...parts.slice(2)]
|
|
8
|
+
|
|
9
|
+
if (possibleEnvironmentList.length === 0) {
|
|
10
|
+
// handle .env1 -> development1
|
|
11
|
+
return filename.replace('.env', 'development')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (possibleEnvironmentList.length === 1) {
|
|
15
|
+
return possibleEnvironmentList[0]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (possibleEnvironmentList.length === 2) {
|
|
19
|
+
return possibleEnvironmentList.join('_')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return possibleEnvironmentList.slice(0, 2).join('_')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function keyNamesForEnvFile (filepath = '.env') {
|
|
26
|
+
const filename = canonicalEnvFilename(filepath)
|
|
27
|
+
|
|
28
|
+
if (filename === '.env') {
|
|
29
|
+
return {
|
|
30
|
+
publicKeyName: 'DOTENV_PUBLIC_KEY',
|
|
31
|
+
privateKeyName: 'DOTENV_PRIVATE_KEY'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const resolvedEnvironment = environment(filename).toUpperCase()
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
publicKeyName: `DOTENV_PUBLIC_KEY_${resolvedEnvironment}`,
|
|
39
|
+
privateKeyName: `DOTENV_PRIVATE_KEY_${resolvedEnvironment}`
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = keyNamesForEnvFile
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
function escapeForRegex (value) {
|
|
5
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function upsertEnvKey (key, value, keysFilepath = '.env.keys') {
|
|
9
|
+
const resolvedKeysFilepath = path.resolve(keysFilepath)
|
|
10
|
+
const keyValueLine = `${key}=${value}`
|
|
11
|
+
const created = !fs.existsSync(resolvedKeysFilepath)
|
|
12
|
+
|
|
13
|
+
let src = ''
|
|
14
|
+
if (!created) {
|
|
15
|
+
src = fs.readFileSync(resolvedKeysFilepath, 'utf8')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const eol = src.includes('\r\n') ? '\r\n' : '\n'
|
|
19
|
+
const keyPattern = new RegExp(`^\\s*(?:export\\s+)?${escapeForRegex(key)}\\s*=`)
|
|
20
|
+
const lines = src.length > 0 ? src.split(/\r?\n/) : []
|
|
21
|
+
|
|
22
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
23
|
+
lines.pop()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let replaced = false
|
|
27
|
+
const nextLines = []
|
|
28
|
+
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (keyPattern.test(line)) {
|
|
31
|
+
if (!replaced) {
|
|
32
|
+
nextLines.push(keyValueLine)
|
|
33
|
+
replaced = true
|
|
34
|
+
}
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
nextLines.push(line)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!replaced) {
|
|
42
|
+
nextLines.push(keyValueLine)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nextSrc = `${nextLines.join(eol)}${eol}`
|
|
46
|
+
const changed = created || nextSrc !== src
|
|
47
|
+
|
|
48
|
+
if (changed) {
|
|
49
|
+
fs.writeFileSync(resolvedKeysFilepath, nextSrc, 'utf8')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
changed,
|
|
54
|
+
key,
|
|
55
|
+
value,
|
|
56
|
+
created,
|
|
57
|
+
filepath: keysFilepath
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = upsertEnvKey
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const dotenvx = require('@dotenvx/dotenvx')
|
|
2
|
+
const PostArmorPull = require('../api/postArmorPull')
|
|
3
|
+
const upsertEnvKey = require('../helpers/upsertEnvKey')
|
|
4
|
+
const keyNamesForEnvFile = require('../helpers/keyNamesForEnvFile')
|
|
5
|
+
|
|
6
|
+
class ArmorPull {
|
|
7
|
+
constructor (hostname, token, devicePublicKey, envFile = '.env') {
|
|
8
|
+
this.hostname = hostname
|
|
9
|
+
this.token = token
|
|
10
|
+
this.devicePublicKey = devicePublicKey
|
|
11
|
+
this.envFile = envFile
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run () {
|
|
15
|
+
const hostname = this.hostname
|
|
16
|
+
const token = this.token
|
|
17
|
+
const devicePublicKey = this.devicePublicKey
|
|
18
|
+
const envFile = this.envFile
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
publicKeyName,
|
|
22
|
+
privateKeyName
|
|
23
|
+
} = keyNamesForEnvFile(envFile)
|
|
24
|
+
|
|
25
|
+
const publicKey = dotenvx.get(publicKeyName, { path: envFile, strict: true, ignore: ['MISSING_PRIVATE_KEY'] })
|
|
26
|
+
const json = await new PostArmorPull(hostname, token, devicePublicKey, publicKey).run()
|
|
27
|
+
|
|
28
|
+
const result = upsertEnvKey(privateKeyName, json.private_key)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...json,
|
|
32
|
+
changed: result.changed,
|
|
33
|
+
privateKeyName,
|
|
34
|
+
privateKeyValue: json.private_key
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = ArmorPull
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const dotenvx = require('@dotenvx/dotenvx')
|
|
2
|
+
const PostArmorPush = require('../api/postArmorPush')
|
|
3
|
+
const keyNamesForEnvFile = require('../helpers/keyNamesForEnvFile')
|
|
4
|
+
|
|
5
|
+
class ArmorPush {
|
|
6
|
+
constructor (hostname, token, devicePublicKey, envFile = '.env') {
|
|
7
|
+
this.hostname = hostname
|
|
8
|
+
this.token = token
|
|
9
|
+
this.devicePublicKey = devicePublicKey
|
|
10
|
+
this.envFile = envFile
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async run () {
|
|
14
|
+
const hostname = this.hostname
|
|
15
|
+
const token = this.token
|
|
16
|
+
const devicePublicKey = this.devicePublicKey
|
|
17
|
+
const envFile = this.envFile
|
|
18
|
+
|
|
19
|
+
const { privateKeyName } = keyNamesForEnvFile(envFile)
|
|
20
|
+
|
|
21
|
+
const privateKey = dotenvx.get(privateKeyName, { path: '.env.keys', strict: true })
|
|
22
|
+
const json = await new PostArmorPush(hostname, token, devicePublicKey, privateKey).run()
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
...json,
|
|
26
|
+
changed: json.changed,
|
|
27
|
+
privateKeyName,
|
|
28
|
+
privateKeyValue: json.private_key
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = ArmorPush
|