@envseal/cli 0.1.2 → 0.1.4
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/dist/bin.js +2 -2
- package/dist/cli-utils.d.ts +1 -0
- package/dist/cli-utils.js +4 -1
- package/dist/commands/doctor.js +7 -8
- package/dist/commands/revoke.d.ts +1 -1
- package/dist/commands/revoke.js +40 -2
- package/dist/commands/run.js +13 -15
- package/dist/exit-codes.js +3 -0
- package/dist/test-prompter.js +16 -8
- package/package.json +8 -8
package/dist/bin.js
CHANGED
|
@@ -13,7 +13,7 @@ import { doctor } from './commands/doctor.js';
|
|
|
13
13
|
import { revoke } from './commands/revoke.js';
|
|
14
14
|
import { mcp } from './commands/mcp.js';
|
|
15
15
|
import { init } from './commands/init.js';
|
|
16
|
-
const VERSION = '0.1.
|
|
16
|
+
const VERSION = '0.1.4';
|
|
17
17
|
async function main() {
|
|
18
18
|
const argv = process.argv.slice(2);
|
|
19
19
|
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
@@ -112,7 +112,7 @@ async function main() {
|
|
|
112
112
|
finish(EXIT.USAGE);
|
|
113
113
|
break;
|
|
114
114
|
}
|
|
115
|
-
await revoke(root, key, json);
|
|
115
|
+
await revoke(root, key, json, parsed.flags.yes === true);
|
|
116
116
|
break;
|
|
117
117
|
}
|
|
118
118
|
case 'mcp': {
|
package/dist/cli-utils.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ type BrokerOpts = ConstructorParameters<typeof Broker>[0];
|
|
|
12
12
|
export declare function createBroker(root: string, opts?: {
|
|
13
13
|
onConfirm?: BrokerOpts['onConfirm'];
|
|
14
14
|
onApprovalNeeded?: BrokerOpts['onApprovalNeeded'];
|
|
15
|
+
onRevokeConfirm?: BrokerOpts['onRevokeConfirm'];
|
|
15
16
|
}): Promise<Broker>;
|
|
16
17
|
/**
|
|
17
18
|
* Whether there is a human we could put a question to.
|
package/dist/cli-utils.js
CHANGED
|
@@ -25,6 +25,7 @@ export async function createBroker(root, opts) {
|
|
|
25
25
|
prompter,
|
|
26
26
|
onConfirm: opts?.onConfirm,
|
|
27
27
|
onApprovalNeeded: opts?.onApprovalNeeded,
|
|
28
|
+
onRevokeConfirm: opts?.onRevokeConfirm,
|
|
28
29
|
});
|
|
29
30
|
registerDisposable(() => broker.dispose());
|
|
30
31
|
return broker;
|
|
@@ -206,10 +207,12 @@ file permissions, missing required keys.
|
|
|
206
207
|
|
|
207
208
|
--json Output as JSON.
|
|
208
209
|
--project <path> Project root (default: auto-detect).`,
|
|
209
|
-
revoke: `Usage: envseal revoke <KEY> [--json] [--project <path>]
|
|
210
|
+
revoke: `Usage: envseal revoke <KEY> [--yes] [--json] [--project <path>]
|
|
210
211
|
|
|
211
212
|
Remove a key from its sink and report the provider's rotation URL.
|
|
213
|
+
Asks for confirmation first; --yes (or ENVSEAL_ASSUME_YES=1) pre-approves it.
|
|
212
214
|
|
|
215
|
+
--yes Skip the confirmation prompt.
|
|
213
216
|
--json Output as JSON.
|
|
214
217
|
--project <path> Project root (default: auto-detect).`,
|
|
215
218
|
mcp: `Usage: envseal mcp
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { SepError } from '@envseal/protocol';
|
|
4
|
+
import { inspectDotenvGitSafety, projectPaths } from '@envseal/core';
|
|
5
5
|
import { emit, fail } from '../output.js';
|
|
6
6
|
import { EXIT } from '../exit-codes.js';
|
|
7
7
|
import { detectHost } from '../host.js';
|
|
@@ -25,12 +25,9 @@ export async function doctor(root, json) {
|
|
|
25
25
|
const status = await broker.describe();
|
|
26
26
|
const gitignorePath = join(root, '.gitignore');
|
|
27
27
|
const envPath = join(root, '.env');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
32
|
-
gitignoreCovers = gitignoreContent.includes('.env');
|
|
33
|
-
}
|
|
28
|
+
const gitSafety = inspectDotenvGitSafety(projectPaths(root));
|
|
29
|
+
const gitignoreCovers = gitSafety.ignored;
|
|
30
|
+
const hookFailClosed = process.env.ENVSEAL_HOOK_FAIL_CLOSED === '1';
|
|
34
31
|
// Check .env permissions
|
|
35
32
|
let envFileOk = false;
|
|
36
33
|
if (existsSync(envPath)) {
|
|
@@ -54,9 +51,10 @@ export async function doctor(root, json) {
|
|
|
54
51
|
},
|
|
55
52
|
envFile: {
|
|
56
53
|
exists: existsSync(envPath),
|
|
57
|
-
isTracked:
|
|
54
|
+
isTracked: gitSafety.tracked,
|
|
58
55
|
permissionsOk: envFileOk,
|
|
59
56
|
},
|
|
57
|
+
hookFailClosed,
|
|
60
58
|
missingRequiredCount: status.missingRequired.length,
|
|
61
59
|
missingRequired: status.missingRequired,
|
|
62
60
|
};
|
|
@@ -66,6 +64,7 @@ export async function doctor(root, json) {
|
|
|
66
64
|
console.log(` ${host.reason}`);
|
|
67
65
|
console.log(` ${host.recommendation}`);
|
|
68
66
|
console.log(`Gitignore covers .env: ${gitignoreCovers ? 'yes' : 'no'}`);
|
|
67
|
+
console.log(`Hook on internal error: ${hookFailClosed ? 'fail-closed' : 'fail-open (default)'}`);
|
|
69
68
|
console.log(`Missing required keys: ${status.missingRequired.length}`);
|
|
70
69
|
if (status.missingRequired.length > 0) {
|
|
71
70
|
for (const key of status.missingRequired) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function revoke(root: string, key: string, json: boolean): Promise<void>;
|
|
1
|
+
export declare function revoke(root: string, key: string, json: boolean, assumeYes?: boolean): Promise<void>;
|
|
2
2
|
//# sourceMappingURL=revoke.d.ts.map
|
package/dist/commands/revoke.js
CHANGED
|
@@ -1,10 +1,48 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { SepError } from '@envseal/protocol';
|
|
3
|
+
import { revokeConfirmationBody } from '@envseal/core';
|
|
1
4
|
import { emit, fail } from '../output.js';
|
|
2
5
|
import { EXIT } from '../exit-codes.js';
|
|
3
6
|
import { createBroker } from '../cli-utils.js';
|
|
4
7
|
import { finish } from '../exit.js';
|
|
5
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Ask the user to approve removing stored credentials.
|
|
10
|
+
*
|
|
11
|
+
* Without this the broker's revoke confirmation callback is absent and
|
|
12
|
+
* `revoke` always throws SEP_CONFIRMATION_DENIED. Mirrors run.ts: same
|
|
13
|
+
* TTY/--yes/ENVSEAL_ASSUME_YES gate, same fail-closed default for headless.
|
|
14
|
+
*/
|
|
15
|
+
async function confirmRevokeInteractive(keys) {
|
|
16
|
+
if (process.env.ENVSEAL_ASSUME_YES === '1')
|
|
17
|
+
return true;
|
|
18
|
+
if (!process.stdin.isTTY) {
|
|
19
|
+
throw new SepError({
|
|
20
|
+
code: 'SEP_NO_INTERACTIVE_SURFACE',
|
|
21
|
+
userMessage: 'envseal revoke needs confirmation before removing stored credentials, but there is no terminal to ask on. ' +
|
|
22
|
+
'Nothing was removed. Re-run it yourself in an interactive shell to review and ' +
|
|
23
|
+
'approve the revocation; see docs/ci.md for the supported headless pipeline setup.',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const body = revokeConfirmationBody(keys, process.cwd());
|
|
27
|
+
process.stderr.write(`\n${body}\n\n`);
|
|
28
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
29
|
+
try {
|
|
30
|
+
const answer = await new Promise((resolve) => {
|
|
31
|
+
rl.question('Continue? [y/N] ', resolve);
|
|
32
|
+
});
|
|
33
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
rl.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function revoke(root, key, json, assumeYes = false) {
|
|
6
40
|
try {
|
|
7
|
-
const broker = await createBroker(root
|
|
41
|
+
const broker = await createBroker(root, {
|
|
42
|
+
onRevokeConfirm: assumeYes
|
|
43
|
+
? async () => true
|
|
44
|
+
: async (keys) => confirmRevokeInteractive(keys),
|
|
45
|
+
});
|
|
8
46
|
const results = await broker.revoke({
|
|
9
47
|
keys: [key],
|
|
10
48
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { SepError } from '@envseal/protocol';
|
|
3
|
+
import { useConfirmationBody } from '@envseal/core';
|
|
3
4
|
import { emit, fail } from '../output.js';
|
|
4
5
|
import { createBroker } from '../cli-utils.js';
|
|
5
6
|
import { loadManifest, projectPaths } from '@envseal/core';
|
|
@@ -12,9 +13,11 @@ import { finish } from '../exit.js';
|
|
|
12
13
|
* prompt reads from the controlling terminal, so it still works when stdout is
|
|
13
14
|
* being piped.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The dialog body comes from the SAME useConfirmationBody the MCP and SDK
|
|
17
|
+
* bindings render — one escaping/fingerprint implementation for every
|
|
18
|
+
* surface. Fails closed: no TTY and no --yes means no approval. That is the
|
|
19
|
+
* right default, because the alternative is a CI job silently handing
|
|
20
|
+
* credentials to whatever it was told to run.
|
|
18
21
|
*/
|
|
19
22
|
async function confirmInteractive(info) {
|
|
20
23
|
if (process.env.ENVSEAL_ASSUME_YES === '1')
|
|
@@ -23,23 +26,18 @@ async function confirmInteractive(info) {
|
|
|
23
26
|
// Distinct from a refusal. Reporting "the user denied the confirmation" when
|
|
24
27
|
// no human was ever asked is actively misleading, and it is the shell-only
|
|
25
28
|
// agents of Tier 4 that hit this path — the ones this binding exists for.
|
|
29
|
+
// Deliberately silent about HOW to proceed headlessly: this message lands in
|
|
30
|
+
// agent-visible stderr, and advertising the bypass here handed every reader
|
|
31
|
+
// a one-liner to skip the dialog entirely (documented in docs/ci.md instead).
|
|
26
32
|
throw new SepError({
|
|
27
33
|
code: 'SEP_NO_INTERACTIVE_SURFACE',
|
|
28
34
|
userMessage: 'envseal run needs confirmation before injecting secrets, but there is no terminal to ask on. ' +
|
|
29
|
-
'Re-run in an interactive shell
|
|
35
|
+
'Nothing was run and no value was read. Re-run it yourself in an interactive shell to review and ' +
|
|
36
|
+
'approve the command; see docs/ci.md for the supported headless pipeline setup.',
|
|
30
37
|
});
|
|
31
38
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
'envseal is about to run a command with secrets in its environment:',
|
|
35
|
-
` command: ${info.command.join(' ')}`,
|
|
36
|
-
` keys: ${info.keys.join(', ')}`,
|
|
37
|
-
];
|
|
38
|
-
if (info.networkEgress) {
|
|
39
|
-
lines.push(' WARNING: this command can make network requests, so it could send', ' these values somewhere. Only continue if you trust it.');
|
|
40
|
-
}
|
|
41
|
-
lines.push('');
|
|
42
|
-
process.stderr.write(lines.join('\n'));
|
|
39
|
+
const body = useConfirmationBody(info, process.cwd());
|
|
40
|
+
process.stderr.write(`\n${body}\n\n`);
|
|
43
41
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
44
42
|
try {
|
|
45
43
|
const answer = await new Promise((resolve) => {
|
package/dist/exit-codes.js
CHANGED
|
@@ -22,6 +22,7 @@ export function exitCodeForError(e) {
|
|
|
22
22
|
case 'SEP_GITIGNORE_UNSAFE':
|
|
23
23
|
case 'SEP_PROBE_NOT_APPROVED':
|
|
24
24
|
case 'SEP_VALUE_IN_REQUEST':
|
|
25
|
+
case 'SEP_PATTERN_UNSAFE':
|
|
25
26
|
return EXIT.USAGE;
|
|
26
27
|
case 'SEP_NO_INTERACTIVE_SURFACE':
|
|
27
28
|
return EXIT.NO_SURFACE;
|
|
@@ -34,7 +35,9 @@ export function exitCodeForError(e) {
|
|
|
34
35
|
case 'SEP_FORMAT_INVALID':
|
|
35
36
|
case 'SEP_RATE_LIMITED':
|
|
36
37
|
case 'SEP_TICKET_UNKNOWN':
|
|
38
|
+
case 'SEP_TARGET_CHANGED':
|
|
37
39
|
case 'SEP_CONFIRMATION_DENIED':
|
|
40
|
+
case 'SEP_KEYS_MISSING':
|
|
38
41
|
return EXIT.UNSATISFIED;
|
|
39
42
|
default: {
|
|
40
43
|
const _exhaustive = code;
|
package/dist/test-prompter.js
CHANGED
|
@@ -17,14 +17,22 @@ export function createStubPrompter(value) {
|
|
|
17
17
|
return {
|
|
18
18
|
id: 'ide',
|
|
19
19
|
available: async () => true,
|
|
20
|
-
prompt: async (req) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
prompt: async (req) => {
|
|
21
|
+
// Announce the stub on stderr, mirroring probe-approval.ts's
|
|
22
|
+
// ENVSEAL_TEST_APPROVAL notice: any answer that did not come from a
|
|
23
|
+
// human must be visible in the transcript. A model that discovers the
|
|
24
|
+
// two-variable gate cannot use it as a silent approval oracle.
|
|
25
|
+
process.stderr.write(`ENVSEAL_TEST_MODE: prompter is a STUB — key(s) [${req.keys.map((k) => k.key).join(', ')}] ` +
|
|
26
|
+
`answered from ENVSEAL_TEST_PROMPTER_VALUE with no UI.\n`);
|
|
27
|
+
return {
|
|
28
|
+
ticket: req.ticket,
|
|
29
|
+
results: req.keys.map((k) => ({
|
|
30
|
+
key: k.key,
|
|
31
|
+
outcome: 'entered',
|
|
32
|
+
value: secretFromUtf8(value),
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
28
36
|
cancel: async () => {
|
|
29
37
|
/* nothing to tear down */
|
|
30
38
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envseal/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"provenance": true
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@envseal/
|
|
27
|
-
"@envseal/
|
|
28
|
-
"@envseal/
|
|
29
|
-
"@envseal/mcp-server": "0.1.
|
|
30
|
-
"@envseal/
|
|
31
|
-
"@envseal/
|
|
32
|
-
"@envseal/
|
|
26
|
+
"@envseal/protocol": "0.1.4",
|
|
27
|
+
"@envseal/core": "0.1.4",
|
|
28
|
+
"@envseal/prompters": "0.1.4",
|
|
29
|
+
"@envseal/mcp-server": "0.1.4",
|
|
30
|
+
"@envseal/detector": "0.1.4",
|
|
31
|
+
"@envseal/http-server": "0.1.4",
|
|
32
|
+
"@envseal/registry": "0.1.4"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|