@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 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.39.1...main)
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.39.1",
2
+ "version": "0.40.0",
3
3
  "name": "@dotenvx/dotenvx-ops",
4
4
  "description": "Secrets for agents–from the creator of `dotenv` and `dotenvx`",
5
5
  "author": "@motdotla",
@@ -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