@eventra_dev/eventra-cli 1.0.2 → 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 +28 -4
- package/dist/cli/init.js +7 -1
- package/dist/cli/send.js +36 -9
- package/dist/config/config.js +81 -1
- package/dist/index.js +3 -2
- package/dist/plugin/loadPlugins.js +21 -1
- package/package.json +1 -1
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-
|
|
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
|
|
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
|
-
| `
|
|
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, **
|
|
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
|
-
|
|
67
|
-
//
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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) {
|
package/dist/config/config.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
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
|
}
|