@envseal/cli 0.1.1 → 0.1.2

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.1';
16
+ const VERSION = '0.1.2';
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') {
@@ -1,8 +1,8 @@
1
+ import { SepError } from '@envseal/protocol';
1
2
  import { emit, fail } from '../output.js';
2
3
  import { EXIT } from '../exit-codes.js';
3
4
  import { loadManifest, projectPaths } from '@envseal/core';
4
- import { SepError } from '@envseal/protocol';
5
- import { createBroker, outcomeForKey } from '../cli-utils.js';
5
+ import { createBroker, hasInteractiveSurface, outcomeForKey } from '../cli-utils.js';
6
6
  import { finish } from '../exit.js';
7
7
  export async function ensure(root, json, check = false) {
8
8
  try {
@@ -80,6 +80,18 @@ export async function ensure(root, json, check = false) {
80
80
  }
81
81
  return;
82
82
  }
83
+ // F2: without --check, ensure asks the user for values. When there is no
84
+ // interactive surface and no double-gated test prompter configured, refuse
85
+ // now instead of selecting the loopback surface and waiting on a browser
86
+ // page nobody can see (the W4 hang). Mirrors run.ts's fail-closed stance.
87
+ if (!hasInteractiveSurface() && !(process.env.ENVSEAL_TEST_PROMPTER_VALUE !== undefined ||
88
+ process.env.ENVSEAL_TEST_PROMPTER_OUTCOME !== undefined)) {
89
+ throw new SepError({
90
+ code: 'SEP_NO_INTERACTIVE_SURFACE',
91
+ userMessage: 'envseal ensure needs to collect missing keys, but there is no interactive surface here. ' +
92
+ 'Use --check to gate without prompting, provide values out of band, or run interactively.',
93
+ });
94
+ }
83
95
  // Request all missing keys at once
84
96
  const ticket = await broker.request({
85
97
  keys: missingRequired,
@@ -1,4 +1,5 @@
1
- import { projectPaths, loadManifest, declareEntries } from '@envseal/core';
1
+ import { projectPaths, loadManifest, declareEntries, scanManifestEntry } from '@envseal/core';
2
+ import { SepError } from '@envseal/protocol';
2
3
  import { emit, fail } from '../output.js';
3
4
  import { detectHost } from '../host.js';
4
5
  import { scanForEnvKeys, entryForKey } from '../scan.js';
@@ -31,6 +32,26 @@ export async function init(root, json, hostOverride) {
31
32
  }
32
33
  const paths = projectPaths(root);
33
34
  const discovered = scanForEnvKeys(root);
35
+ // F1: a manifest that already exists was authored by someone else (an
36
+ // editor, another tool, a malicious repo). Declaring into it without
37
+ // validating it first would launder hostile entries — e.g. a
38
+ // secret-shaped format.example — through envseal's own write path and
39
+ // commit them as blessed. Re-run the same schema+guard validation the
40
+ // declare path uses over every entry already on disk.
41
+ const existing = loadManifest(paths);
42
+ if (existing !== null) {
43
+ for (const [index, entry] of existing.entries.entries()) {
44
+ const finding = scanManifestEntry(entry, `entries[${index}]`);
45
+ if (finding !== null) {
46
+ throw new SepError({
47
+ code: 'SEP_VALUE_IN_REQUEST',
48
+ details: { field: finding.path },
49
+ userMessage: `Refusing to init: the existing ${finding.path} entry looks like it contains a real credential (${finding.label}). ` +
50
+ 'Manifest fields are committed to git and must describe keys, never contain values.',
51
+ });
52
+ }
53
+ }
54
+ }
34
55
  const entries = discovered.map(entryForKey);
35
56
  // declareEntries creates the manifest when absent and edits it surgically
36
57
  // when present, so re-running init on an existing project is safe and
@@ -2,6 +2,7 @@ import { createInterface } from 'node:readline';
2
2
  import { SepError } from '@envseal/protocol';
3
3
  import { emit, fail } from '../output.js';
4
4
  import { createBroker } from '../cli-utils.js';
5
+ import { loadManifest, projectPaths } from '@envseal/core';
5
6
  import { finish } from '../exit.js';
6
7
  /**
7
8
  * Ask the user to approve running a command with secrets injected.
@@ -55,6 +56,15 @@ export async function run(root, command, json, assumeYes = false) {
55
56
  const broker = await createBroker(root, {
56
57
  onConfirm: assumeYes ? async () => true : confirmInteractive,
57
58
  });
59
+ // F4: a missing manifest while values still resolve is a suspicious state
60
+ // (deleted by hand or tooling). The run proceeds — the .env is user-owned —
61
+ // but never silently: the condition goes to stderr in both output modes.
62
+ if (loadManifest(projectPaths(root)) === null) {
63
+ const warning = 'envseal: no env.schema.jsonc found for this project. ' +
64
+ 'Values are still resolved from .env/keychain because those are user-owned, ' +
65
+ "but nothing is declared here anymore. Run `envseal init` to re-create the manifest.";
66
+ process.stderr.write(`\n${warning}\n\n`);
67
+ }
58
68
  const status = await broker.describe();
59
69
  const presentKeys = status.entries.filter((e) => e.present).map((e) => e.key);
60
70
  const result = await broker.use({ keys: presentKeys, command });
@@ -1,8 +1,9 @@
1
1
  import { emit, fail } from '../output.js';
2
2
  import { EXIT, exitCodeForOutcome } from '../exit-codes.js';
3
- import { createBroker, outcomeForKey } from '../cli-utils.js';
3
+ import { createBroker, hasInteractiveSurface, outcomeForKey } from '../cli-utils.js';
4
4
  import { finish } from '../exit.js';
5
5
  import { loadManifest, projectPaths, saveManifest } from '@envseal/core';
6
+ import { SepError } from '@envseal/protocol';
6
7
  /**
7
8
  * Undo a declaration THIS invocation added, after nothing was stored under it.
8
9
  *
@@ -37,6 +38,27 @@ export async function set(root, key, json) {
37
38
  let declaredHere = false;
38
39
  try {
39
40
  const broker = await createBroker(root);
41
+ // F2: same fail-closed rule as ensure/run. Without this, a non-TTY caller
42
+ // (agent shell, scheduler) selected the loopback surface and waited on a
43
+ // browser page nobody could see.
44
+ //
45
+ // The guard fires BEFORE any declare, so a refused run also leaves no
46
+ // phantom declaration — but only for keys that are NOT already declared
47
+ // (those keep the pre-existing rollback semantics below). CI=1 keeps the
48
+ // original path: declare-then-request so the refusal is announced with the
49
+ // "declared X but nothing was stored" mutation message, which the contract
50
+ // test pins.
51
+ const surfaceUsable = hasInteractiveSurface() ||
52
+ process.env.CI !== undefined ||
53
+ process.env.ENVSEAL_TEST_PROMPTER_VALUE !== undefined ||
54
+ process.env.ENVSEAL_TEST_PROMPTER_OUTCOME !== undefined;
55
+ if (!surfaceUsable) {
56
+ throw new SepError({
57
+ code: 'SEP_NO_INTERACTIVE_SURFACE',
58
+ userMessage: 'envseal set needs to collect a value, but there is no interactive surface here. ' +
59
+ 'Run it in an interactive shell, or use the documented ENVSEAL_TEST_MODE hooks in tests.',
60
+ });
61
+ }
40
62
  // Declare the key only if it is not already in the manifest.
41
63
  //
42
64
  // `declareEntries` replaces an entry wholesale rather than merging, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envseal/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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/protocol": "0.1.1",
27
- "@envseal/core": "0.1.1",
28
- "@envseal/prompters": "0.1.1",
29
- "@envseal/registry": "0.1.1",
30
- "@envseal/mcp-server": "0.1.1",
31
- "@envseal/http-server": "0.1.1",
32
- "@envseal/detector": "0.1.1"
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"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",