@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 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.2';
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': {
@@ -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
@@ -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
- // Check gitignore
29
- let gitignoreCovers = false;
30
- if (existsSync(gitignorePath)) {
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: false,
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
@@ -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
- export async function revoke(root, key, json) {
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
  });
@@ -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
- * Fails closed: no TTY and no --yes means no approval. That is the right
16
- * default, because the alternative is a CI job silently handing credentials to
17
- * whatever it was told to run.
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, or pass --yes (or set ENVSEAL_ASSUME_YES=1) to approve non-interactively.',
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 lines = [
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) => {
@@ -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;
@@ -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
- ticket: req.ticket,
22
- results: req.keys.map((k) => ({
23
- key: k.key,
24
- outcome: 'entered',
25
- value: secretFromUtf8(value),
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.2",
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/prompters": "0.1.2",
27
- "@envseal/registry": "0.1.2",
28
- "@envseal/detector": "0.1.2",
29
- "@envseal/mcp-server": "0.1.2",
30
- "@envseal/protocol": "0.1.2",
31
- "@envseal/core": "0.1.2",
32
- "@envseal/http-server": "0.1.2"
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",