@eventra_dev/eventra-cli 1.0.3 → 1.0.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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm version" src="https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
9
9
  <a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
10
10
  <a href="https://github.com/and-1991/eventra-cli/actions/workflows/test.yml"><img alt="tests" src="https://img.shields.io/github/actions/workflow/status/and-1991/eventra-cli/test.yml?branch=main&label=tests&style=flat-square&logo=vitest&logoColor=white"></a>
11
- <img alt="unit tests" src="https://img.shields.io/badge/unit-59%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
11
+ <img alt="unit tests" src="https://img.shields.io/badge/unit-74%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
12
12
  <img alt="e2e fixtures" src="https://img.shields.io/badge/e2e-12%20fixtures-brightgreen?style=flat-square">
13
13
  <img alt="node" src="https://img.shields.io/node/v/@eventra_dev/eventra-cli?style=flat-square&color=darkgreen&logo=node.js&logoColor=white">
14
14
  <a href="https://www.typescriptlang.org/"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-ready-blue?style=flat-square&logo=typescript&logoColor=white"></a>
@@ -155,7 +155,23 @@ Incremental scan with the same rules as `sync`.
155
155
 
156
156
  ### `eventra send`
157
157
 
158
- Uploads `events` from config to the Eventra API. Requires `apiKey` (and optionally `endpoint`) in `eventra.json`. Events are POSTed to `POST /api/v1/cli/events` and marked as non-billable on the backend.
158
+ Uploads `events` from config to the Eventra API. Requires an API key and (optionally) an `endpoint`. Events are POSTed to `POST /api/v1/cli/events` and marked as non-billable on the backend.
159
+
160
+ **API key resolution** (checked in this order, never written to `eventra.json`):
161
+
162
+ 1. `EVENTRA_API_KEY` environment variable — recommended for CI
163
+ 2. `eventra.local.json` (created by `eventra init`/`eventra send`, automatically gitignored)
164
+ 3. legacy inline `apiKey` in `eventra.json` (deprecated — avoid committing real keys here)
165
+
166
+ `eventra.json` is meant to be committed so CI can diff it via `eventra check`; keeping the key out of it avoids leaking it into source control. In a non-interactive shell (no TTY), `send` fails fast with a message pointing at `EVENTRA_API_KEY` instead of hanging on a prompt.
167
+
168
+ **Endpoint trust.** The default production endpoint is always used with no extra step. A *custom* `endpoint` is only trusted without approval when it comes from the `EVENTRA_ENDPOINT` environment variable (setting an env var is already a local/CI action). A custom `endpoint` committed inline in `eventra.json` is trust-on-first-use: `send` refuses to run until it's approved once with:
169
+
170
+ ```bash
171
+ eventra send --trust-endpoint
172
+ ```
173
+
174
+ This records the endpoint in `eventra.local.json` (gitignored). If `eventra.json`'s `endpoint` later changes to a different value — e.g. a PR silently pointing it at another host — `send` blocks again until re-approved, so a committed config change alone can't redirect where your API key and events get sent.
159
175
 
160
176
  Network resilience:
161
177
 
@@ -224,6 +240,8 @@ External plugins export an object (or factory) with:
224
240
 
225
241
  No dependency on `@eventra_dev/eventra-cli` is required. See [`ARCHITECTURE.md`](./ARCHITECTURE.md#plugin-kernel-phase-15--foundation-shipped) for details.
226
242
 
243
+ **Only official `@eventra_dev/cli-plugin-*` packages can be loaded.** `eventra.json` is meant to be committed to git, so its `plugins` array is effectively PR-editable; since the CLI `import()`s whatever is listed there, an arbitrary specifier would be arbitrary code execution on every machine that runs `sync`/`check`/`watch`. Because `@eventra_dev` is an npm scope only the Eventra maintainers can publish to, restricting to `@eventra_dev/cli-plugin-*` means nothing outside that scope can ever be loaded this way. Any other entry in `plugins` is skipped with a console warning instead of being imported — third-party/community plugins aren't supported today.
244
+
227
245
  ---
228
246
 
229
247
  ## Configuration
@@ -244,10 +262,14 @@ No dependency on `@eventra_dev/eventra-cli` is required. See [`ARCHITECTURE.md`]
244
262
 
245
263
  | Field | Description |
246
264
  |-------|-------------|
247
- | `plugins` | Module specifiers to `import()` at startup (e.g. `@eventra_dev/cli-plugin-vue`) |
265
+ | `apiKey` | Legacy inline API key — leave empty. Use `EVENTRA_API_KEY` or `eventra.local.json` instead (see [`eventra send`](#eventra-send)) so a real key never ends up in this committed file |
266
+ | `endpoint` | Custom `eventra send` target (e.g. self-hosted). Non-default values from this file need one-time local approval — see [`eventra send`](#eventra-send) |
267
+ | `plugins` | `@eventra_dev/cli-plugin-*` specifiers to `import()` at startup. Anything outside that scope is skipped (see [Plugin contract](#plugin-contract-for-authors)) |
248
268
  | `sync.include` | Base glob patterns; plugin `includeGlobs` are merged automatically |
249
269
  | `sync.exclude` | Paths skipped during scan |
250
270
 
271
+ `eventra init`/`eventra send` never write a real key into this file — they write to `eventra.local.json` instead, which is automatically added to `.gitignore`.
272
+
251
273
  ---
252
274
 
253
275
  ## How it works
@@ -275,7 +297,7 @@ No runtime execution. No monkey-patching.
275
297
 
276
298
  ## Test Coverage
277
299
 
278
- Two test layers, **59 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios**.
300
+ Two test layers, **74 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios**.
279
301
 
280
302
  **Unit tests (vitest)** — 13 suites covering core modules:
281
303
 
@@ -322,6 +344,8 @@ pnpm --filter @eventra_dev/eventra-cli test:unit # vitest only
322
344
  pnpm --filter @eventra_dev/eventra-cli test:e2e # fixtures + check exit codes
323
345
  ```
324
346
 
347
+ > **Node version note:** the CLI itself supports Node 18+ (see `engines`), but `vitest@4` depends on `node:util`'s `styleText`, which requires Node **≥ 20.12** — running the test suite on an older Node 20.x patch (or Node 18) fails to start. CI runs the matrix on Node 20 and 22; locally, use the version pinned in the repo's `.nvmrc`.
348
+
325
349
  ---
326
350
 
327
351
  ## License
package/dist/cli/init.js CHANGED
@@ -29,7 +29,7 @@ async function init() {
29
29
  console.log(chalk_1.default.gray("• cross-file propagation chains"));
30
30
  console.log("");
31
31
  const config = {
32
- apiKey,
32
+ apiKey: "",
33
33
  endpoint,
34
34
  events: [],
35
35
  functionWrappers: [],
@@ -47,6 +47,12 @@ async function init() {
47
47
  };
48
48
  await (0, config_1.saveConfig)(config);
49
49
  console.log(chalk_1.default.green("eventra.json created"));
50
+ // Never write the key into eventra.json — that file is meant to be
51
+ // committed (CI diffs it via `eventra check`). Keep secrets out of git.
52
+ if (apiKey) {
53
+ await (0, config_1.saveLocalApiKey)(apiKey);
54
+ console.log(chalk_1.default.green("API key saved to eventra.local.json (added to .gitignore)"));
55
+ }
50
56
  console.log("");
51
57
  console.log(chalk_1.default.gray("Run `eventra sync`"));
52
58
  }
package/dist/cli/send.js CHANGED
@@ -9,6 +9,8 @@ const inquirer_1 = __importDefault(require("inquirer"));
9
9
  const config_1 = require("../config/config");
10
10
  const package_json_1 = __importDefault(require("../../package.json"));
11
11
  const CLI_VERSION = package_json_1.default.version;
12
+ // Always trusted, no local approval needed — matches `eventra init`'s prompt default.
13
+ const DEFAULT_SEND_ENDPOINT = "https://api.eventra.dev/api/v1/cli/events";
12
14
  // retry/backoff tuning (mirrors SDK defaults)
13
15
  const MAX_ATTEMPTS = 4;
14
16
  const BASE_DELAY_MS = 500;
@@ -56,16 +58,40 @@ async function attemptDelivery(endpoint, apiKey, payload) {
56
58
  clearTimeout(timeout);
57
59
  }
58
60
  }
59
- async function send() {
61
+ async function send(opts = {}) {
60
62
  const config = await (0, config_1.loadConfig)();
61
- const endpoint = config?.endpoint || process.env.EVENTRA_ENDPOINT;
62
63
  if (!config) {
63
64
  console.log(chalk_1.default.red("eventra.json not found. Run 'eventra init'"));
64
65
  return;
65
66
  }
66
- let apiKey = config.apiKey;
67
- // ask api key
67
+ const endpoint = config.endpoint || process.env.EVENTRA_ENDPOINT || undefined;
68
+ // A non-default endpoint sourced from the committed eventra.json (not from
69
+ // EVENTRA_ENDPOINT, which already requires a local/CI env-var change) needs
70
+ // one-time local approval — otherwise a PR silently editing `endpoint` could
71
+ // redirect send (and the API key) to an attacker's server.
72
+ if (endpoint &&
73
+ endpoint !== DEFAULT_SEND_ENDPOINT &&
74
+ endpoint === config.endpoint &&
75
+ !process.env.EVENTRA_ENDPOINT) {
76
+ const trusted = await (0, config_1.getTrustedEndpoint)();
77
+ if (endpoint !== trusted) {
78
+ if (!opts.trustEndpoint) {
79
+ console.log(chalk_1.default.red(`Endpoint "${endpoint}" from eventra.json is not locally approved.`));
80
+ console.log(chalk_1.default.gray("If this is expected (e.g. a self-hosted ingest URL), approve it once with:"));
81
+ console.log(chalk_1.default.gray(" eventra send --trust-endpoint"));
82
+ return;
83
+ }
84
+ await (0, config_1.trustEndpoint)(endpoint);
85
+ console.log(chalk_1.default.green(`Endpoint "${endpoint}" approved and saved to eventra.local.json`));
86
+ }
87
+ }
88
+ let apiKey = await (0, config_1.resolveApiKey)(config);
89
+ // ask for the key only when interactive — never hang a CI/non-TTY run on a prompt
68
90
  if (!apiKey) {
91
+ if (!process.stdin.isTTY) {
92
+ console.log(chalk_1.default.red("API key required. Set the EVENTRA_API_KEY environment variable (recommended for CI) or run 'eventra init'."));
93
+ return;
94
+ }
69
95
  const answers = await inquirer_1.default.prompt([
70
96
  {
71
97
  type: "input",
@@ -74,11 +100,12 @@ async function send() {
74
100
  }
75
101
  ]);
76
102
  apiKey = answers.apiKey;
77
- await (0, config_1.saveConfig)({
78
- ...config,
79
- apiKey,
80
- });
81
- console.log(chalk_1.default.green("API key saved"));
103
+ if (apiKey) {
104
+ // Saved to the gitignored local secrets file, never to eventra.json —
105
+ // that file is meant to be committed for `eventra check` drift detection.
106
+ await (0, config_1.saveLocalApiKey)(apiKey);
107
+ console.log(chalk_1.default.green("API key saved to eventra.local.json (added to .gitignore)"));
108
+ }
82
109
  }
83
110
  // no events
84
111
  if (!config.events?.length) {
@@ -3,13 +3,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CONFIG_NAME = void 0;
6
+ exports.LOCAL_CONFIG_NAME = exports.CONFIG_NAME = void 0;
7
7
  exports.normalizeConfig = normalizeConfig;
8
8
  exports.loadConfig = loadConfig;
9
9
  exports.saveConfig = saveConfig;
10
+ exports.resolveApiKey = resolveApiKey;
11
+ exports.saveLocalApiKey = saveLocalApiKey;
12
+ exports.getTrustedEndpoint = getTrustedEndpoint;
13
+ exports.trustEndpoint = trustEndpoint;
10
14
  const fs_extra_1 = __importDefault(require("fs-extra"));
11
15
  const path_1 = __importDefault(require("path"));
12
16
  exports.CONFIG_NAME = "eventra.json";
17
+ /**
18
+ * Secrets live here instead of `eventra.json`, since the latter is meant to be
19
+ * committed (CI diffs `events`/`functionWrappers` against it via `eventra check`).
20
+ */
21
+ exports.LOCAL_CONFIG_NAME = "eventra.local.json";
13
22
  const DEFAULT_INCLUDE = ["**/*.{ts,tsx,js,jsx}"];
14
23
  const DEFAULT_EXCLUDE = ["node_modules", "dist", ".next", ".git"];
15
24
  function normalizeConfig(config) {
@@ -52,3 +61,74 @@ async function saveConfig(config) {
52
61
  const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
53
62
  await fs_extra_1.default.writeJSON(configPath, normalizeConfig(config), { spaces: 2 });
54
63
  }
64
+ async function readLocalSecrets() {
65
+ const localPath = path_1.default.join(process.cwd(), exports.LOCAL_CONFIG_NAME);
66
+ if (!(await fs_extra_1.default.pathExists(localPath)))
67
+ return {};
68
+ try {
69
+ const parsed = await fs_extra_1.default.readJSON(localPath);
70
+ return {
71
+ apiKey: typeof parsed?.apiKey === "string" && parsed.apiKey ? parsed.apiKey : undefined,
72
+ trustedEndpoint: typeof parsed?.trustedEndpoint === "string" && parsed.trustedEndpoint
73
+ ? parsed.trustedEndpoint
74
+ : undefined,
75
+ };
76
+ }
77
+ catch {
78
+ return {};
79
+ }
80
+ }
81
+ async function writeLocalSecrets(patch) {
82
+ const localPath = path_1.default.join(process.cwd(), exports.LOCAL_CONFIG_NAME);
83
+ const next = { ...(await readLocalSecrets()), ...patch };
84
+ const out = {};
85
+ if (next.apiKey)
86
+ out.apiKey = next.apiKey;
87
+ if (next.trustedEndpoint)
88
+ out.trustedEndpoint = next.trustedEndpoint;
89
+ await fs_extra_1.default.writeJSON(localPath, out, { spaces: 2 });
90
+ await ensureGitignored(exports.LOCAL_CONFIG_NAME);
91
+ }
92
+ async function ensureGitignored(entry) {
93
+ const gitignorePath = path_1.default.join(process.cwd(), ".gitignore");
94
+ let content = "";
95
+ if (await fs_extra_1.default.pathExists(gitignorePath)) {
96
+ content = await fs_extra_1.default.readFile(gitignorePath, "utf8");
97
+ if (content.split(/\r?\n/).some((line) => line.trim() === entry))
98
+ return;
99
+ }
100
+ const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
101
+ await fs_extra_1.default.appendFile(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}\n`);
102
+ }
103
+ /**
104
+ * Resolves the API key for outbound requests only — never persisted back onto
105
+ * the `EventraConfig` object, so it can't leak into `eventra.json` via `saveConfig`.
106
+ * Priority: `EVENTRA_API_KEY` env var > `eventra.local.json` (gitignored) >
107
+ * legacy inline `apiKey` in `eventra.json` (kept for backward compatibility only).
108
+ */
109
+ async function resolveApiKey(config) {
110
+ if (process.env.EVENTRA_API_KEY)
111
+ return process.env.EVENTRA_API_KEY;
112
+ const { apiKey } = await readLocalSecrets();
113
+ if (apiKey)
114
+ return apiKey;
115
+ return config.apiKey || undefined;
116
+ }
117
+ /** Persists an API key to the gitignored local secrets file, never to `eventra.json`. */
118
+ async function saveLocalApiKey(apiKey) {
119
+ await writeLocalSecrets({ apiKey });
120
+ }
121
+ /**
122
+ * The `endpoint` a locally-run `eventra send` last approved, trust-on-first-use
123
+ * style — a non-default `endpoint` sourced from the *committed* `eventra.json`
124
+ * must match this before it's used, so a PR silently changing it can't redirect
125
+ * `send` (and the API key with it) without a human approving it locally once.
126
+ */
127
+ async function getTrustedEndpoint() {
128
+ const { trustedEndpoint } = await readLocalSecrets();
129
+ return trustedEndpoint;
130
+ }
131
+ /** Records `endpoint` as locally approved (see `getTrustedEndpoint`). */
132
+ async function trustEndpoint(endpoint) {
133
+ await writeLocalSecrets({ trustedEndpoint: endpoint });
134
+ }
package/dist/index.js CHANGED
@@ -72,8 +72,9 @@ program
72
72
  });
73
73
  program
74
74
  .command("send")
75
- .action(async () => {
75
+ .option("--trust-endpoint", "approve the current eventra.json endpoint for local use")
76
+ .action(async (opts) => {
76
77
  const { send } = await Promise.resolve().then(() => __importStar(require("./cli/send")));
77
- await send();
78
+ await send({ trustEndpoint: opts.trustEndpoint });
78
79
  });
79
80
  program.parse();
@@ -33,11 +33,24 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isTrustedPluginSpecifier = isTrustedPluginSpecifier;
36
37
  exports.createBuiltinPluginRegistry = createBuiltinPluginRegistry;
37
38
  exports.createPluginRegistry = createPluginRegistry;
38
39
  const external_1 = require("./adapters/external");
39
40
  const eventra_sdk_1 = require("./builtins/eventra-sdk");
40
41
  const registry_1 = require("./registry");
42
+ /**
43
+ * Only packages published under the `@eventra_dev` npm scope can ever be
44
+ * `import()`-ed as a plugin. `eventra.json` (and its `plugins` array) is meant
45
+ * to be committed to git, so anything else listed there is attacker-editable
46
+ * via a plain PR — restricting to a scope only the maintainers can publish to
47
+ * turns "arbitrary code execution via a one-line JSON edit" into "not possible
48
+ * without also compromising the @eventra_dev npm account."
49
+ */
50
+ const TRUSTED_PLUGIN_RE = /^@eventra_dev\/cli-plugin-[a-z0-9]+(?:-[a-z0-9]+)*$/;
51
+ function isTrustedPluginSpecifier(spec) {
52
+ return TRUSTED_PLUGIN_RE.test(spec);
53
+ }
41
54
  async function resolvePluginExport(mod) {
42
55
  let candidate = mod.default ?? mod;
43
56
  if (typeof candidate === "function") {
@@ -82,7 +95,14 @@ function createBuiltinPluginRegistry() {
82
95
  async function createPluginRegistry(config) {
83
96
  const registry = createBuiltinPluginRegistry();
84
97
  for (const spec of config.plugins ?? []) {
85
- await loadExternalPlugin(registry, spec);
98
+ const trimmed = spec.trim();
99
+ if (!trimmed)
100
+ continue;
101
+ if (!isTrustedPluginSpecifier(trimmed)) {
102
+ console.warn(`Eventra: skipping plugin "${trimmed}" — only official @eventra_dev/cli-plugin-* packages can be loaded.${pluginLoadHint(trimmed)}`);
103
+ continue;
104
+ }
105
+ await loadExternalPlugin(registry, trimmed);
86
106
  }
87
107
  return registry;
88
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",