keep pull: fill saved template with real values by default
All checks were successful
Docker / build-and-push (push) Successful in 1m54s
All checks were successful
Docker / build-and-push (push) Successful in 1m54s
A bare key/value dump throws away the comments/structure a template preserves; now a plain pull reconstitutes the real .env from the template when the vault has one, falling back to the flat dump otherwise. --template still returns the redacted structure alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
11
README.md
11
README.md
@@ -124,14 +124,19 @@ Comments explaining *where a value comes from* or *why it exists* are
|
|||||||
exactly the kind of context a bare key/value pair throws away, so this
|
exactly the kind of context a bare key/value pair throws away, so this
|
||||||
keeps it without keeping the secret itself around unencrypted anywhere.
|
keeps it without keeping the secret itself around unencrypted anywhere.
|
||||||
|
|
||||||
|
A plain `keep pull` uses that template by default: if the vault has one,
|
||||||
|
you get real values filled back into it, comments and all — not just a
|
||||||
|
bare key/value dump. Ask for the redacted structure itself with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
keep pull myapp/production --template > .env.example
|
keep pull myapp/production --template > .env.example
|
||||||
```
|
```
|
||||||
|
|
||||||
Vaults pushed before this existed just don't have a saved template —
|
Vaults pushed before this existed just don't have a saved template —
|
||||||
`keep pull --template` on one of those fails with a clear error instead
|
`keep pull` on one of those falls back to a plain key/value dump, and
|
||||||
of silently returning nothing. No migration needed; push again and the
|
`keep pull --template` fails with a clear error instead of silently
|
||||||
template gets backfilled from whatever `.env` you push next.
|
returning nothing. No migration needed; push again and the template
|
||||||
|
gets backfilled from whatever `.env` you push next.
|
||||||
|
|
||||||
## Granting, revoking, rotating
|
## Granting, revoking, rotating
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { requireIdentityAndRecipientId } from '../identityStore.js';
|
import { requireIdentityAndRecipientId } from '../identityStore.js';
|
||||||
import { signedFetch, adminFetch, expectOk } from '../signedFetch.js';
|
import { signedFetch, adminFetch, expectOk } from '../signedFetch.js';
|
||||||
import { parseEnv, serializeEnv, buildEnvTemplate } from '../envFormat.js';
|
import { parseEnv, serializeEnv, buildEnvTemplate, fillEnvTemplate } from '../envFormat.js';
|
||||||
import {
|
import {
|
||||||
ready,
|
ready,
|
||||||
Identity,
|
Identity,
|
||||||
@@ -89,7 +89,9 @@ export async function vaultPull(vaultKey: string, format: 'env' | 'json', outFil
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const out = format === 'json' ? JSON.stringify(values, null, 2) : serializeEnv(values);
|
const out = format === 'json'
|
||||||
|
? JSON.stringify(values, null, 2)
|
||||||
|
: template !== undefined ? fillEnvTemplate(template, values) : serializeEnv(values);
|
||||||
if (outFile) {
|
if (outFile) {
|
||||||
fs.writeFileSync(outFile, out);
|
fs.writeFileSync(outFile, out);
|
||||||
console.error(`wrote ${Object.keys(values).length} key(s) to ${outFile}${previous ? ' (previous version)' : ''}`);
|
console.error(`wrote ${Object.keys(values).length} key(s) to ${outFile}${previous ? ' (previous version)' : ''}`);
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ export function serializeEnv(values: Record<string, string>): string {
|
|||||||
// — the whole point is to preserve the "why"/"where to get this" context
|
// — the whole point is to preserve the "why"/"where to get this" context
|
||||||
// that plain parseEnv() throws away, without keeping the actual secret
|
// that plain parseEnv() throws away, without keeping the actual secret
|
||||||
// around in the template.
|
// around in the template.
|
||||||
|
// Inverse of buildEnvTemplate: substitutes real values back into a
|
||||||
|
// template's `KEY=` lines while leaving comments/blank lines/order intact.
|
||||||
|
export function fillEnvTemplate(template: string, values: Record<string, string>): string {
|
||||||
|
return template
|
||||||
|
.split('\n')
|
||||||
|
.map(line => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) return line;
|
||||||
|
const eq = line.indexOf('=');
|
||||||
|
if (eq === -1) return line;
|
||||||
|
const key = line.slice(0, eq);
|
||||||
|
const value = values[key.trim()] ?? '';
|
||||||
|
return `${key}=${/[\s#"']/.test(value) ? JSON.stringify(value) : value}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
export function buildEnvTemplate(text: string): string {
|
export function buildEnvTemplate(text: string): string {
|
||||||
return text
|
return text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ function printUsage(): void {
|
|||||||
|
|
||||||
keep push <vault> [--file .env] [--purge] (--purge: don't retain the outgoing version as rollback)
|
keep push <vault> [--file .env] [--purge] (--purge: don't retain the outgoing version as rollback)
|
||||||
keep pull <vault> [--format env|json] [--out <path>] [--previous] [--template]
|
keep pull <vault> [--format env|json] [--out <path>] [--previous] [--template]
|
||||||
(--template: comments/structure with values redacted, not the secrets)
|
(fills the saved template with real values by default, if one exists;
|
||||||
|
--template: comments/structure with values redacted, not the secrets)
|
||||||
keep grant <vault> <recipient-id> [--read-only]
|
keep grant <vault> <recipient-id> [--read-only]
|
||||||
keep revoke <vault> <recipient-id> (admin — needs KEEP_ADMIN_PASSWORD)
|
keep revoke <vault> <recipient-id> (admin — needs KEEP_ADMIN_PASSWORD)
|
||||||
keep log <vault>
|
keep log <vault>
|
||||||
|
|||||||
Reference in New Issue
Block a user