onesecret 0.0.0 → 0.1.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/src/write.js CHANGED
@@ -172,20 +172,24 @@ const updateLayer = (options, layer, update) => {
172
172
  return writeLayer(current, update({ ...current.tree }))
173
173
  }
174
174
 
175
- const parseAssignment = assignment => {
176
- const separator = assignment.indexOf('=')
177
- if (separator < 1) {
178
- throw new OnesecretError(`"${assignment}" is not NAME=value`, 'EVALUE')
175
+ const set = (options, layer, name, readValue) => {
176
+ const current = readLayer(options, layer)
177
+ const { rules } = encryptionFor(current.file, current.existing)
178
+ assertWritable({ [name]: '' }, [name], rules)
179
+ const previous = current.tree[name]
180
+ const shown =
181
+ previous === undefined || previous === null ? '' : String(previous)
182
+ const value = readValue(shown)
183
+ if (previous !== undefined && previous !== null && shown === value) {
184
+ return { file: current.file, status: 'unchanged' }
185
+ }
186
+ if (value === null || value === '') {
187
+ return { file: current.file, status: 'cancelled' }
179
188
  }
180
- return [assignment.slice(0, separator), assignment.slice(separator + 1)]
189
+ writeLayer(current, { ...current.tree, [name]: value })
190
+ return { file: current.file, status: 'encrypted' }
181
191
  }
182
192
 
183
- const set = (options, layer, assignments) =>
184
- updateLayer(options, layer, tree => ({
185
- ...tree,
186
- ...Object.fromEntries(assignments.map(parseAssignment))
187
- }))
188
-
189
193
  const unset = (options, layer, secretNames) =>
190
194
  updateLayer(options, layer, tree => {
191
195
  for (const name of secretNames) {
@@ -234,7 +238,9 @@ const initialRecipients = defaultFile => {
234
238
  const rule = store.creationRule(defaultFile)
235
239
  if (rule) return { recipients: rule.recipients, source: store.SOPS_CONFIG }
236
240
  const exported = exportedRecipients()
237
- if (exported.length > 0) { return { recipients: exported, source: 'SOPS_AGE_KEY' } }
241
+ if (exported.length > 0) {
242
+ return { recipients: exported, source: 'SOPS_AGE_KEY' }
243
+ }
238
244
  return null
239
245
  }
240
246
 
package/src/deploy.js DELETED
@@ -1,113 +0,0 @@
1
- 'use strict'
2
-
3
- const { execFileSync } = require('node:child_process')
4
- const yaml = require('js-yaml')
5
- const { loadSecrets, readLayers } = require('./secrets')
6
- const { withoutAgeKey } = require('./keys')
7
- const { checksum, toEnv } = require('./format')
8
- const flyEncoding = require('./fly')
9
- const { OnesecretError } = require('./error')
10
-
11
- const MANAGED_BY_LABEL = 'app.kubernetes.io/managed-by'
12
-
13
- const loadForDeploy = (target, options) => {
14
- if (!options.env) {
15
- throw new OnesecretError(
16
- `${target} needs an explicit env, such as "production"`,
17
- 'EVALUE'
18
- )
19
- }
20
- const layered = readLayers(options)
21
- const envLayer = layered.layers.find(({ name }) => name === layered.env)
22
- if (!envLayer.plainFile && !envLayer.encryptedFile) {
23
- throw new OnesecretError(
24
- `${target} found no "${layered.env}" layer in ${layered.dir}, pass --env default to deploy the default layer alone`,
25
- 'EVALUE'
26
- )
27
- }
28
- return loadSecrets(layered)
29
- }
30
-
31
- const run = (binary, args, input) => {
32
- try {
33
- return execFileSync(binary, args, {
34
- input,
35
- encoding: 'utf8',
36
- env: withoutAgeKey(process.env),
37
- stdio: ['pipe', 'pipe', 'pipe']
38
- })
39
- } catch (error) {
40
- const output = [error.stdout, error.stderr].filter(Boolean).join('\n')
41
- const redacted = output.replace(/=.*/g, '=***').trim()
42
- const reason =
43
- error.code === 'ENOENT'
44
- ? `${binary} is not installed`
45
- : redacted || error.message
46
- throw new OnesecretError(
47
- `${binary} ${args.slice(0, 3).join(' ')} failed: ${reason}`,
48
- 'EDEPLOY'
49
- )
50
- }
51
- }
52
-
53
- const kubernetesManifest = ({ name, namespace, secrets }) =>
54
- yaml.dump(
55
- {
56
- apiVersion: 'v1',
57
- kind: 'Secret',
58
- metadata: {
59
- name,
60
- ...(namespace && { namespace }),
61
- labels: { [MANAGED_BY_LABEL]: 'onesecret' }
62
- },
63
- type: 'Opaque',
64
- data: Object.fromEntries(
65
- Object.entries(toEnv(secrets)).map(([key, value]) => [
66
- key,
67
- Buffer.from(value).toString('base64')
68
- ])
69
- )
70
- },
71
- { lineWidth: -1 }
72
- )
73
-
74
- const kubernetes = ({
75
- name,
76
- namespace,
77
- context,
78
- kubectl = 'kubectl',
79
- args = [],
80
- ...options
81
- }) => {
82
- if (!name) {
83
- throw new OnesecretError('kubernetes needs the Secret name', 'EVALUE')
84
- }
85
- const secrets = loadForDeploy('kubernetes', options)
86
- const contextArgs = context ? ['--context', context] : []
87
- const output = run(
88
- kubectl,
89
- [...contextArgs, ...args, 'apply', '-f', '-'],
90
- kubernetesManifest({ name, namespace, secrets })
91
- )
92
- return { checksum: checksum(secrets), output: output.trim() }
93
- }
94
-
95
- const flyImport = secrets => flyEncoding.encode(toEnv(secrets))
96
-
97
- const fly = ({ app, stage = true, flyctl = 'fly', ...options }) => {
98
- if (!app) throw new OnesecretError('fly needs the app name', 'EVALUE')
99
- const secrets = loadForDeploy('fly', options)
100
- const stageArgs = stage ? ['--stage'] : []
101
- const output = run(
102
- flyctl,
103
- ['secrets', 'import', ...stageArgs, '--app', app],
104
- flyImport(secrets)
105
- )
106
- const summary = output
107
- .split('\n')
108
- .filter(line => line.trim() && !line.includes('='))
109
- .join('\n')
110
- return { checksum: checksum(secrets), output: summary }
111
- }
112
-
113
- module.exports = { fly, flyImport, kubernetes, kubernetesManifest }
package/src/fly.js DELETED
@@ -1,109 +0,0 @@
1
- 'use strict'
2
-
3
- const { isDeepStrictEqual } = require('node:util')
4
- const { OnesecretError } = require('./error')
5
-
6
- const TRIPLE_QUOTE = '"""'
7
- const SCANNER_MAX_LINE_BYTES = 64 * 1024 - 1
8
-
9
- const scanLines = text => {
10
- const lines = text.split('\n')
11
- if (lines[lines.length - 1] === '') lines.pop()
12
- return lines.map(line => line.replace(/\r$/, ''))
13
- }
14
-
15
- const countQuotes = text => text.split('"').length - 1
16
-
17
- const parseSinglelineValue = line => {
18
- const separator = line.indexOf('=')
19
- let value = line.slice(separator + 1).replace(/^ +/, '')
20
- const hash = value.indexOf('#')
21
- if (hash !== -1 && countQuotes(value.slice(0, hash)) % 2 === 0) { value = value.slice(0, hash).replace(/ +$/, '') }
22
- return { key: line.slice(0, separator).trim(), value }
23
- }
24
-
25
- const parse = text => {
26
- const secrets = {}
27
- let multiline = null
28
- for (const line of scanLines(text)) {
29
- if (multiline) {
30
- if (line.endsWith(TRIPLE_QUOTE)) {
31
- secrets[multiline.key] =
32
- multiline.value + line.slice(0, -TRIPLE_QUOTE.length)
33
- multiline = null
34
- } else {
35
- multiline.value += `${line}\n`
36
- }
37
- continue
38
- }
39
- if (line.startsWith('#') || line.trim() === '') continue
40
- if (!line.includes('=')) throw new Error(`"${line}" is not NAME=VALUE`)
41
- const { key, value } = parseSinglelineValue(line)
42
- if (
43
- value.startsWith(TRIPLE_QUOTE) &&
44
- value.endsWith(TRIPLE_QUOTE) &&
45
- value.length >= 6
46
- ) {
47
- secrets[key] = value.slice(3, -3)
48
- } else if (value.startsWith(TRIPLE_QUOTE)) {
49
- multiline = { key, value: `${value.slice(3)}\n` }
50
- } else if (
51
- (value.startsWith('"') && value.endsWith('"')) ||
52
- (value.startsWith("'") && value.endsWith("'"))
53
- ) {
54
- if (value.length < 2) throw new Error('flyctl panics on a lone quote')
55
- secrets[key] = value.slice(1, -1)
56
- } else {
57
- secrets[key] = value
58
- }
59
- }
60
- return secrets
61
- }
62
-
63
- const candidates = value => [
64
- value,
65
- `${TRIPLE_QUOTE}${value}${TRIPLE_QUOTE}`,
66
- `"${value}"`,
67
- `'${value}'`
68
- ]
69
-
70
- const readsBackAs = (text, expected) => {
71
- try {
72
- return isDeepStrictEqual(parse(text), expected)
73
- } catch {
74
- return false
75
- }
76
- }
77
-
78
- const isWithinScanner = text =>
79
- scanLines(text).every(
80
- line => Buffer.byteLength(line) <= SCANNER_MAX_LINE_BYTES
81
- )
82
-
83
- const encodeLine = (name, value) => {
84
- const line = candidates(value)
85
- .map(encoded => `${name}=${encoded}\n`)
86
- .find(line => isWithinScanner(line) && readsBackAs(line, { [name]: value }))
87
- if (!line) {
88
- throw new OnesecretError(
89
- `"${name}" cannot be imported into fly without changing its value`,
90
- 'EVALUE'
91
- )
92
- }
93
- return line
94
- }
95
-
96
- const encode = secrets => {
97
- const body = Object.entries(secrets)
98
- .map(([name, value]) => encodeLine(name, value))
99
- .join('')
100
- if (!readsBackAs(body, secrets)) {
101
- throw new OnesecretError(
102
- 'the fly import does not read back to the same secrets',
103
- 'EVALUE'
104
- )
105
- }
106
- return body
107
- }
108
-
109
- module.exports = { encode, parse }