sops-env 0.0.0-stage → 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/dist/cli.mjs +119 -0
  4. package/package.json +57 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilya Semenov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # sops-env
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Print the `env` section of a [SOPS](https://github.com/getsops/sops)-encrypted file as `KEY=VALUE` lines.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 22 or later, or Bun
8
+ - [`sops`](https://github.com/getsops/sops) in `PATH`, with access to the decryption key
9
+
10
+ ## Usage
11
+
12
+ Keep the environment variables under the `env` key of a SOPS file; other keys are ignored.
13
+ An encrypted file `environment.sops.yaml` looks like this:
14
+
15
+ ```yaml
16
+ env:
17
+ NODE_ENV: ENC[AES256_GCM,data:66pExK+Z2weH0g==,iv:…,tag:…,type:str]
18
+ PORT: ENC[AES256_GCM,data:8hoeUQ==,iv:…,tag:…,type:int]
19
+ SECRET_KEY: ENC[AES256_GCM,data:4+CiTc2C,iv:…,tag:…,type:str]
20
+ sops:
21
+ # Encrypted data keys and metadata
22
+ ```
23
+
24
+ Print the decrypted variables as lines of an environment file:
25
+
26
+ ```sh
27
+ npx sops-env environment.sops.yaml
28
+ ```
29
+
30
+ ```sh
31
+ NODE_ENV=production
32
+ PORT=3000
33
+ SECRET_KEY=s3cr3t
34
+ ```
35
+
36
+ Values are printed as is, without quoting or escaping, so the command fails when a value spans several lines.
37
+
38
+ ### Set variables in Dokku
39
+
40
+ Pass `--base64` to print every value base64-encoded, as `dokku config:set --encoded` expects; multi-line values are allowed:
41
+
42
+ ```sh
43
+ ssh dokku@example.com config:set --encoded my-app $(npx sops-env --base64 environment.sops.yaml)
44
+ ```
45
+
46
+ The unquoted substitution splits the output into one argument per variable.
47
+ A failed command inside it does not stop the script, so in a script assign the output to a variable first and stop on failure, instead of running `config:set` without variables:
48
+
49
+ ```sh
50
+ config=$(npx sops-env --base64 environment.sops.yaml) || exit
51
+ ssh dokku@example.com config:set --encoded my-app $config
52
+ ```
53
+
54
+ ### Values
55
+
56
+ - Strings, numbers, and booleans are printed as text, such as `PORT=3000` and `DEBUG=true`.
57
+ - sops normalizes numbers, such as `1.50` to `1.5`; quote a value to keep its exact text.
58
+ - `null`, lists, and maps are rejected: quote a value, such as `EMPTY: ''`, to print it as text.
package/dist/cli.mjs ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { spawnSync } from "node:child_process";
4
+ import { constants } from "node:os";
5
+ // src/error.ts
6
+ /** Error that the CLI reports to the user as a message without a stack trace. */
7
+ var SopsEnvError = class extends Error {
8
+ exitCode;
9
+ constructor(message, exitCode = 1) {
10
+ super(message);
11
+ this.exitCode = exitCode;
12
+ this.name = "SopsEnvError";
13
+ }
14
+ };
15
+ // src/env.ts
16
+ /**
17
+ * Formats the `env` section of a decrypted document as `KEY=VALUE` lines.
18
+ *
19
+ * Without `base64`, values are written as is, without quoting, so multi-line values are rejected.
20
+ */
21
+ function formatEnv(document, { base64 = false } = {}) {
22
+ const env = isRecord(document) ? document.env : void 0;
23
+ if (!isRecord(env)) throw new SopsEnvError("The env section is missing or is not a mapping.");
24
+ const entries = Object.entries(env).map(([key, value]) => [key, toEnvValue(key, value)]);
25
+ if (!base64) {
26
+ const multiline = entries.filter(([, value]) => value.includes("\n")).map(([key]) => key);
27
+ if (multiline.length) throw new SopsEnvError(`Multi-line values are not supported: ${multiline.join(", ")}`);
28
+ }
29
+ return entries.map(([key, value]) => `${key}=${base64 ? Buffer.from(value).toString("base64") : value}`).join("\n");
30
+ }
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ /** Converts a scalar value to its string form; rejects null and collections. */
35
+ function toEnvValue(key, value) {
36
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
37
+ throw new SopsEnvError(`Unsupported value of ${key}: expected a string, number, or boolean.`);
38
+ }
39
+ // src/sops.ts
40
+ /**
41
+ * Decrypts a SOPS file with the `sops` CLI and returns the parsed document.
42
+ *
43
+ * Numbers are returned as their JSON source text, so that integers beyond the double precision keep every digit.
44
+ * Throws a `SopsEnvError` with the exit code that a shell would report for sops:
45
+ * its own exit code, 128 plus the signal number when a signal kills it, or 127 when it is missing.
46
+ * Only a missing sops gets a message; stderr of sops is inherited.
47
+ */
48
+ function decryptFile(path) {
49
+ const result = spawnSync("sops", [
50
+ "decrypt",
51
+ "--output-type",
52
+ "json",
53
+ path
54
+ ], {
55
+ encoding: "utf8",
56
+ maxBuffer: Number.POSITIVE_INFINITY,
57
+ stdio: [
58
+ "ignore",
59
+ "pipe",
60
+ "inherit"
61
+ ]
62
+ });
63
+ if (result.error) {
64
+ if (result.error.code === "ENOENT") throw new SopsEnvError("sops is not installed or not in PATH.", 127);
65
+ throw result.error;
66
+ }
67
+ if (result.signal) throw new SopsEnvError("", 128 + constants.signals[result.signal]);
68
+ if (result.status !== 0) throw new SopsEnvError("", result.status ?? 1);
69
+ return parseJson(result.stdout);
70
+ }
71
+ /** Parses JSON, returning numbers as their source text. */
72
+ function parseJson(text) {
73
+ return JSON.parse(text, (_key, value, context) => typeof value === "number" && context?.source !== void 0 ? context.source : value);
74
+ }
75
+ // src/cli.ts
76
+ const usage = "Usage: sops-env [--base64] <environment.sops.yaml>";
77
+ const help = `${usage}
78
+
79
+ Print the env section of a SOPS-encrypted file as KEY=VALUE lines.
80
+ Multi-line values are rejected.
81
+
82
+ Options:
83
+ --base64 Encode values in base64; allows multi-line values
84
+ -h, --help Show this help
85
+ `;
86
+ function main(args) {
87
+ let parsed;
88
+ try {
89
+ parsed = parseArgs({
90
+ args,
91
+ allowPositionals: true,
92
+ options: {
93
+ base64: { type: "boolean" },
94
+ help: {
95
+ type: "boolean",
96
+ short: "h"
97
+ }
98
+ }
99
+ });
100
+ } catch (error) {
101
+ throw new SopsEnvError(`${error.message}\n${usage}`);
102
+ }
103
+ const { values, positionals } = parsed;
104
+ if (values.help) {
105
+ process.stdout.write(help);
106
+ return;
107
+ }
108
+ if (positionals.length !== 1) throw new SopsEnvError(usage);
109
+ const output = formatEnv(decryptFile(positionals[0]), { base64: values.base64 });
110
+ if (output) process.stdout.write(`${output}\n`);
111
+ }
112
+ try {
113
+ main(process.argv.slice(2));
114
+ } catch (error) {
115
+ if (!(error instanceof SopsEnvError)) throw error;
116
+ if (error.message) process.stderr.write(`${error.message}\n`);
117
+ process.exitCode = error.exitCode;
118
+ }
119
+ export {};
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "sops-env",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Print the env section of a SOPS-encrypted file as KEY=VALUE lines, optionally with base64-encoded values.",
5
+ "keywords": [
6
+ "cli",
7
+ "dokku",
8
+ "dotenv",
9
+ "env",
10
+ "secrets",
11
+ "sops"
12
+ ],
13
+ "homepage": "https://github.com/IlyaSemenov/sops-env#readme",
14
+ "bugs": "https://github.com/IlyaSemenov/sops-env/issues",
15
+ "license": "MIT",
16
+ "author": "Ilya Semenov",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/IlyaSemenov/sops-env.git"
20
+ },
21
+ "bin": {
22
+ "sops-env": "./dist/cli.mjs"
23
+ },
24
+ "files": [
25
+ "README.md",
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "lint": "oxlint --fix && oxfmt",
35
+ "lint:check": "oxlint && oxfmt --check",
36
+ "prepare": "lefthook install",
37
+ "prepublishOnly": "bun run build",
38
+ "release": "changesets-local",
39
+ "release:check": "bun run build && bun run types && bun run test",
40
+ "test": "bun test",
41
+ "types": "bun check && bun check -p tests"
42
+ },
43
+ "devDependencies": {
44
+ "@changesets/cli": "^3.0.3",
45
+ "@ilyasemenov/oxc-config": "^0.1.3",
46
+ "@tsconfig/bun": "^1.0.12",
47
+ "@types/bun": "^1.4.4",
48
+ "changesets-local": "^1.6.0",
49
+ "oxfmt": "^0.78.0",
50
+ "oxlint": "^1.93.0",
51
+ "publint": "^0.3.25",
52
+ "tsdown": "^0.23.7",
53
+ "typescript": "^7.0.2"
54
+ },
55
+ "engines": {
56
+ "node": ">=22"
57
+ },
58
+ "packageManager": "bun@1.4.4"
59
+ }