sops-env 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -10,6 +10,7 @@ Print the `env` section of a [SOPS](https://github.com/getsops/sops)-encrypted f
10
10
  ## Usage
11
11
 
12
12
  Keep the environment variables under the `env` key of a SOPS file; other keys are ignored.
13
+ Name the variables with ASCII letters, digits, and `_`, and do not start a name with a digit.
13
14
  An encrypted file `environment.sops.yaml` looks like this:
14
15
 
15
16
  ```yaml
@@ -54,5 +55,5 @@ ssh dokku@example.com config:set --encoded my-app $config
54
55
  ### Values
55
56
 
56
57
  - 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
+ - Quote a value to keep its exact text: sops rewrites unquoted numbers and dates, such as `1.50` to `1.5`, `0x10` to `16`, and `2024-01-01` to `2024-01-01T00:00:00Z`.
58
59
  - `null`, lists, and maps are rejected: quote a value, such as `EMPTY: ''`, to print it as text.
package/dist/cli.mjs CHANGED
@@ -16,11 +16,14 @@ var SopsEnvError = class extends Error {
16
16
  /**
17
17
  * Formats the `env` section of a decrypted document as `KEY=VALUE` lines.
18
18
  *
19
+ * Names are written as is and must be shell identifiers, so that no name makes a line ambiguous.
19
20
  * Without `base64`, values are written as is, without quoting, so multi-line values are rejected.
20
21
  */
21
22
  function formatEnv(document, { base64 = false } = {}) {
22
23
  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
+ if (!isRecord(env)) throw new SopsEnvError("The env section is missing or is not a mapping. Put the variables under the env key of a YAML or JSON file; dotenv files are not supported.");
25
+ const invalidNames = Object.keys(env).filter((key) => !isValidName(key));
26
+ if (invalidNames.length) throw new SopsEnvError(`Unsupported variable names: ${invalidNames.map(quoteName).join(", ")}. A name must consist of ASCII letters, digits, and _, and must not start with a digit.`);
24
27
  const entries = Object.entries(env).map(([key, value]) => [key, toEnvValue(key, value)]);
25
28
  if (!base64) {
26
29
  const multiline = entries.filter(([, value]) => value.includes("\n")).map(([key]) => key);
@@ -28,6 +31,24 @@ function formatEnv(document, { base64 = false } = {}) {
28
31
  }
29
32
  return entries.map(([key, value]) => `${key}=${base64 ? Buffer.from(value).toString("base64") : value}`).join("\n");
30
33
  }
34
+ /**
35
+ * Checks that a variable name is a shell identifier: ASCII letters, digits, and `_`, not starting with a digit.
36
+ *
37
+ * Shells and Dokku accept only such names, and they cannot make a `KEY=VALUE` line ambiguous,
38
+ * unlike names with `=`, line breaks, whitespace, or a leading `#`.
39
+ */
40
+ function isValidName(name) {
41
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
42
+ }
43
+ /**
44
+ * Quotes a variable name for an error message.
45
+ *
46
+ * Escapes non-space whitespace, control characters, and format characters left literal by `JSON.stringify`,
47
+ * as `\uXXXX` per UTF-16 code unit, so that a character beyond U+FFFF becomes a surrogate pair.
48
+ */
49
+ function quoteName(name) {
50
+ return JSON.stringify(name).replace(/[^\S ]|[\p{Cc}\p{Cf}]/gu, (char) => char.split("").map((unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`).join(""));
51
+ }
31
52
  function isRecord(value) {
32
53
  return typeof value === "object" && value !== null && !Array.isArray(value);
33
54
  }
@@ -83,22 +104,23 @@ Options:
83
104
  --base64 Encode values in base64; allows multi-line values
84
105
  -h, --help Show this help
85
106
  `;
107
+ const options = {
108
+ base64: { type: "boolean" },
109
+ help: {
110
+ type: "boolean",
111
+ short: "h"
112
+ }
113
+ };
86
114
  function main(args) {
87
115
  let parsed;
88
116
  try {
89
117
  parsed = parseArgs({
90
118
  args,
91
119
  allowPositionals: true,
92
- options: {
93
- base64: { type: "boolean" },
94
- help: {
95
- type: "boolean",
96
- short: "h"
97
- }
98
- }
120
+ options
99
121
  });
100
122
  } catch (error) {
101
- throw new SopsEnvError(`${error.message}\n${usage}`);
123
+ throw new SopsEnvError(`${error.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? `Unknown option '${findUnknownOption(args)}'.` : error.message}\n${usage}`);
102
124
  }
103
125
  const { values, positionals } = parsed;
104
126
  if (values.help) {
@@ -109,6 +131,17 @@ function main(args) {
109
131
  const output = formatEnv(decryptFile(positionals[0]), { base64: values.base64 });
110
132
  if (output) process.stdout.write(`${output}\n`);
111
133
  }
134
+ /** Returns the first option in `args` that the CLI does not define, as the user wrote it. */
135
+ function findUnknownOption(args) {
136
+ const { tokens } = parseArgs({
137
+ args,
138
+ allowPositionals: true,
139
+ options,
140
+ strict: false,
141
+ tokens: true
142
+ });
143
+ for (const token of tokens) if (token.kind === "option" && !Object.hasOwn(options, token.name)) return token.rawName;
144
+ }
112
145
  try {
113
146
  main(process.argv.slice(2));
114
147
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sops-env",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Print the env section of a SOPS-encrypted file as KEY=VALUE lines, optionally with base64-encoded values.",
5
5
  "keywords": [
6
6
  "cli",