@bremmdev/arca-env 0.0.0-stage → 0.1.1

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
@@ -1,3 +1,111 @@
1
- # Temporary Holding Version
1
+ # arca
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Keep secrets out of `.env` files, and out of reach of AI agents.**
4
+
5
+ arca moves the secrets in your `.env` / `.env.local` into an encrypted store outside the project, and hands them to your app only when it runs. Your app still reads `process.env.DATABASE_URL`, so Next.js, Vite, Astro, TanStack Start, Nuxt, SvelteKit, Prisma and plain Node keep working. Non-sensitive config (`PORT`, `NEXT_PUBLIC_*`, feature flags) can stay in `.env`.
6
+
7
+ - **No secret files in the repo.** Values live in an AES-256-GCM store in your user data folder. The key sits in your OS keychain, so there's no password to type.
8
+ - **Masked output, always.** Anything your app prints goes through arca, which replaces secrets with `[arca:DATABASE_URL]`, including the encoded forms and the password inside a connection string. Inside your app's Node processes, a console guard (preloaded through `NODE_OPTIONS`) masks `console.*` arguments too, so dev tools that forward server logs to the browser (such as TanStack Devtools) only see `[arca:NAME]`.
9
+ - **Agents can run your app, not read your secrets.** AI agents can use secrets only through your `package.json` scripts. Only a person at a terminal can reveal or change values.
10
+
11
+ > The goal is **much safer than `.env` files**, not unbreakable. Anything running as your user can, with deliberate effort, reach what you can reach. arca closes the easy, accidental and "helpful agent" paths.
12
+
13
+ ## Quick start
14
+
15
+ ```console
16
+ npm i -D @bremmdev/arca-env
17
+ npx arca init
18
+ npm run dev
19
+ ```
20
+
21
+ `arca init` creates `arca.json` (variable names only, committed), sorts the variables in your `.env` files into secrets and config, imports the secrets, and wraps your scripts:
22
+
23
+ ```json
24
+ {
25
+ "scripts": {
26
+ "dev": "arca run -- next dev",
27
+ "build": "arca run -- next build"
28
+ }
29
+ }
30
+ ```
31
+
32
+ Your `.env` files are left untouched. Once you've checked everything works, remove the plaintext copies with:
33
+
34
+ ```console
35
+ npx arca import --delete # removes only the imported entries; config lines stay
36
+ ```
37
+
38
+ A teammate who clones the repo runs `npm run dev`, gets told which variables are missing, and runs `npx arca setup`.
39
+
40
+ ## Commands
41
+
42
+ arca is installed per project, so in a terminal you run it as `npx arca <command>`. A bare `arca` gives "command not found", because `node_modules/.bin` isn't on your PATH. Inside `package.json` scripts, plain `arca run -- …` works, because npm adds that folder for scripts. For a bare `arca` everywhere, also install it globally: `npm i -g @bremmdev/arca-env`.
43
+
44
+ | Command | What it does | AI agents |
45
+ |---|---|---|
46
+ | `arca run [--only A,B] -- <cmd>` | Run with secrets injected, output masked | ✅ only through `package.json` scripts |
47
+ | `arca list` | Every variable and where it comes from (arca, `.env`, missing). Names only. | ✅ |
48
+ | `arca check [--strict]` | Exit 1 when required variables are missing. `--strict` also fails on secrets in `.env` files. | ✅ |
49
+ | `arca doctor` | Keychain, project, scripts and leak-guard diagnosis | ✅ |
50
+ | `arca set NAME [--generate]` | Store a value: hidden prompt, `… \| arca set NAME`, or a random value | ❌ person only |
51
+ | `arca setup [--all]` | Prompt for every missing required variable | ❌ person only |
52
+ | `arca unset NAME` / `arca restore NAME` | Remove a value / undo the last change | ❌ person only |
53
+ | `arca reveal NAME` / `arca export` | Show one value / print all (`--format dotenv\|json\|shell`, `--output file`) | ❌ person only |
54
+ | `arca import [files] [--only A,B] [--delete]` | Move secrets from `.env` files into arca | ❌ person only |
55
+ | `arca init [--yes] [--new-id]` | Set up a project | ✅ without the import |
56
+ | `arca policy set agent-run deny` | Stop agents from getting secrets at all in this project | ❌ person only |
57
+ | `arca projects [rm <id>]` | Projects with secrets on this machine | list ✅ |
58
+
59
+ ## `arca.json`
60
+
61
+ ```json
62
+ {
63
+ "$schema": "./node_modules/@bremmdev/arca-env/schema.json",
64
+ "id": "arc_7k2m9q4x8w1z3p5n",
65
+ "name": "acme-web",
66
+ "vars": {
67
+ "DATABASE_URL": { "required": true, "description": "Local Postgres", "example": "postgres://postgres:postgres@localhost:5432/acme" },
68
+ "STRIPE_SECRET_KEY": { "required": true, "docs": "https://dashboard.stripe.com/test/apikeys" },
69
+ "AUTH_SECRET": { "required": true, "generate": { "bytes": 32 } },
70
+ "NEXT_PUBLIC_POSTHOG_KEY": { "sensitive": false }
71
+ }
72
+ }
73
+ ```
74
+
75
+ - `sensitive` defaults to `true`: the value comes from arca and is masked. `false` marks config that lives in `.env`.
76
+ - The `id` ties clones and git worktrees of the repo to the same secrets.
77
+ - `envFiles` (opt-in) lists `.env` files that arca loads itself, for tools that don't (plain `node`, `tsx`). Leave it empty for frameworks that load their own.
78
+ - `leakGuard.ignore` silences the leak guard for specific names.
79
+
80
+ ## Secrets in arca, config in `.env`
81
+
82
+ Who wins when a name is in more than one place: **arca, then your shell, then `.env` files**. When arca overrides a different value from your shell, it says so (by name, never by value). `--no-override` flips that for one run.
83
+
84
+ The **leak guard** checks your `.env*` files on every run. It warns when one contains a value stored in arca, a variable declared as a secret, or something that looks like a secret (`sk_live_…`, a URL with a password, a long random string). `arca doctor` shows details and the fix. `arca check --strict` makes it fail, for CI or a pre-commit hook.
85
+
86
+ ## How agents are recognised
87
+
88
+ Agent tools set environment variables in the commands they run: `AI_AGENT` (a shared convention), `CLAUDECODE`, `CURSOR_AGENT`, `CODEX_SANDBOX`, `GEMINI_CLI`, `OPENCODE` and others. You can add your own markers in the store's `config.json` (`"agentMarkers": ["MY_AGENT"]`), or set `ARCA_AGENT=1` in an agent's config.
89
+
90
+ For a detected agent, `arca run` checks that it was started by a `package.json` script whose text matches the file, and that the command is the one in the script. Callers with no marker and no terminal (IDE tasks, git hooks) can run anything, masked. Showing or changing values needs a real terminal and no agent marker. If a marker leaks into your own terminal, arca asks you to type a short code instead.
91
+
92
+ ## CI and deploys
93
+
94
+ There's no store in CI, on Vercel or in Docker, so `arca run` just runs the command with the platform's variables, after checking that the required ones exist. `start` scripts stay unwrapped, so production images don't need arca.
95
+
96
+ ## Where things are kept
97
+
98
+ | What | Where |
99
+ |---|---|
100
+ | Encrypted vaults | `%LOCALAPPDATA%\arca` (Windows), `~/Library/Application Support/arca` (macOS), `~/.local/share/arca` (Linux). Override with `ARCA_HOME`. |
101
+ | Master key | Windows Credential Manager, macOS Keychain or Linux Secret Service. Where there's no persistent keychain (WSL, headless Linux), arca uses a passphrase (`ARCA_KEYCHAIN=passphrase`). |
102
+
103
+ If the keychain entry is lost, the vaults can't be decrypted. For local development secrets that's usually fine, because they can be fetched again from their dashboards.
104
+
105
+ ## Limits
106
+
107
+ arca can't stop deliberate workarounds: an agent that hides its markers, edits `package.json` to add a script that prints secrets (that shows in your git diff), or reads the keychain directly. It also can't stop malware running as you, or your own app reading `process.env`. Other output your app sends to the browser (an error overlay or page that shows a value) or writes to files isn't masked.
108
+
109
+ ## Status
110
+
111
+ Version 0.1: one set of values per project. Profiles (several sets per project, switched with `arca use`), a PTY mode for fully interactive tools, an in-process loader for IDE test runners, and `arca scan` are planned.
package/dist/cli.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { n as main, s as processIO } from "./main-Cy_OrS0c.mjs";
3
+ //#region src/cli.ts
4
+ const code = await main(process.argv.slice(2), processIO());
5
+ process.exitCode = code;
6
+ //#endregion
7
+ export {};
package/dist/guard.mjs ADDED
@@ -0,0 +1,90 @@
1
+ import { r as needlesFor, t as Redactor } from "./redact-D4Txfinc.mjs";
2
+ import { inspect } from "node:util";
3
+ //#region src/guard.ts
4
+ const METHODS = /* @__PURE__ */ new Set([
5
+ "log",
6
+ "info",
7
+ "warn",
8
+ "error",
9
+ "debug",
10
+ "trace",
11
+ "dir",
12
+ "table",
13
+ "dirxml",
14
+ "group",
15
+ "groupCollapsed",
16
+ "assert"
17
+ ]);
18
+ const WRAPPED = Symbol.for("arca.console-guard");
19
+ function installConsoleGuard(secrets, host = globalThis) {
20
+ const needles = secrets.flatMap(needlesFor);
21
+ if (needles.length === 0) return false;
22
+ const redactor = new Redactor(needles);
23
+ const maskArg = (arg) => {
24
+ if (typeof arg === "string") return redactor.redact(arg);
25
+ if (arg === null || typeof arg !== "object" && typeof arg !== "function") return arg;
26
+ let text;
27
+ try {
28
+ text = inspect(arg, {
29
+ depth: 6,
30
+ breakLength: Infinity
31
+ });
32
+ } catch {
33
+ return arg;
34
+ }
35
+ const masked = redactor.redact(text);
36
+ return masked === text ? arg : masked;
37
+ };
38
+ const wrap = (fn) => {
39
+ if (typeof fn !== "function" || fn[WRAPPED]) return fn;
40
+ const original = fn;
41
+ const wrapped = function(...args) {
42
+ return original.apply(this, args.map(maskArg));
43
+ };
44
+ Object.defineProperty(wrapped, "name", { value: original.name });
45
+ wrapped[WRAPPED] = true;
46
+ return wrapped;
47
+ };
48
+ const target = host.console;
49
+ for (const method of METHODS) {
50
+ const d = Object.getOwnPropertyDescriptor(target, method);
51
+ if (d && "value" in d && (d.writable || d.configurable)) Object.defineProperty(target, method, {
52
+ ...d,
53
+ value: wrap(d.value)
54
+ });
55
+ }
56
+ const guarded = new Proxy(target, {
57
+ set(t, prop, value, receiver) {
58
+ return Reflect.set(t, prop, typeof prop === "string" && METHODS.has(prop) ? wrap(value) : value, receiver === guarded ? t : receiver);
59
+ },
60
+ defineProperty(t, prop, descriptor) {
61
+ if (typeof prop === "string" && METHODS.has(prop) && "value" in descriptor) descriptor = {
62
+ ...descriptor,
63
+ value: wrap(descriptor.value)
64
+ };
65
+ return Reflect.defineProperty(t, prop, descriptor);
66
+ }
67
+ });
68
+ const d = Object.getOwnPropertyDescriptor(host, "console");
69
+ Object.defineProperty(host, "console", {
70
+ value: guarded,
71
+ writable: d?.writable ?? true,
72
+ enumerable: d?.enumerable ?? false,
73
+ configurable: true
74
+ });
75
+ return true;
76
+ }
77
+ /** Entry point when preloaded: the names come from ARCA_MASK, the values from this process's env. */
78
+ function installFromEnv(env = process.env) {
79
+ return installConsoleGuard((env.ARCA_MASK ?? "").split(",").filter(Boolean).map((name) => ({
80
+ name,
81
+ value: env[name] ?? ""
82
+ })));
83
+ }
84
+ //#endregion
85
+ //#region src/guard-entry.ts
86
+ try {
87
+ installFromEnv();
88
+ } catch {}
89
+ //#endregion
90
+ export {};
@@ -0,0 +1,119 @@
1
+ //#region src/io.d.ts
2
+ interface OutStream {
3
+ write(chunk: string): void;
4
+ isTTY: boolean;
5
+ columns?: number;
6
+ rows?: number;
7
+ colorDepth?: number;
8
+ }
9
+ interface Prompts {
10
+ /** Masked input. Returns null when the user cancels. */
11
+ password(message: string): Promise<string | null>;
12
+ text(message: string, opts?: {
13
+ placeholder?: string;
14
+ initial?: string;
15
+ }): Promise<string | null>;
16
+ confirm(message: string, initial?: boolean): Promise<boolean | null>;
17
+ multiselect<T extends string>(message: string, options: {
18
+ value: T;
19
+ label: string;
20
+ hint?: string;
21
+ }[], initial: T[]): Promise<T[] | null>;
22
+ }
23
+ interface IO {
24
+ env: NodeJS.ProcessEnv;
25
+ cwd: string;
26
+ platform: NodeJS.Platform;
27
+ stdout: OutStream;
28
+ stderr: OutStream;
29
+ stdinIsTTY: boolean;
30
+ /** Reads all of stdin (used for `… | arca set NAME`). */
31
+ readStdin(): Promise<string>;
32
+ prompts: Prompts;
33
+ /** `inherit` for real terminals, `ignore` in tests. */
34
+ childStdin: 'inherit' | 'ignore';
35
+ /** Only the real CLI forwards signals to the child. */
36
+ handleSignals: boolean;
37
+ }
38
+ declare function processIO(): IO;
39
+ //#endregion
40
+ //#region src/main.d.ts
41
+ declare const VERSION = "0.1.0";
42
+ declare function main(argv: string[], io: IO): Promise<number>;
43
+ //#endregion
44
+ //#region src/redact.d.ts
45
+ interface Needle {
46
+ text: string;
47
+ replacement: string;
48
+ }
49
+ interface Secret {
50
+ name: string;
51
+ value: string;
52
+ }
53
+ /** Builds needles for a value: raw, URL-encoded, JSON-escaped, base64, base64url and URL credentials. */
54
+ declare function needlesFor(secret: Secret): Needle[];
55
+ declare class Redactor {
56
+ private byFirst;
57
+ private held;
58
+ readonly size: number;
59
+ constructor(needles: Needle[]);
60
+ /** Masks a complete string (no streaming). */
61
+ redact(text: string): string;
62
+ /** Feeds a chunk; returns what can be printed now. */
63
+ push(chunk: string): string;
64
+ /** Returns the held tail, masked as far as possible. */
65
+ flush(): string;
66
+ get pending(): boolean;
67
+ private scan;
68
+ }
69
+ /** Wires a byte stream through a Redactor into `write`, with the 50 ms hold-back flush. */
70
+ declare function createMaskedWriter(redactor: Redactor, write: (s: string) => void, flushMs?: number): {
71
+ write(chunk: Buffer | string): void;
72
+ end(): void;
73
+ };
74
+ //#endregion
75
+ //#region src/envfiles.d.ts
76
+ interface EnvEntry {
77
+ key: string;
78
+ value: string;
79
+ /** First and last line (0-based, inclusive) of the entry in the file. */
80
+ startLine: number;
81
+ endLine: number;
82
+ }
83
+ declare function parseEnvEntries(text: string): EnvEntry[];
84
+ declare function parseEnv(text: string): Record<string, string>;
85
+ //#endregion
86
+ //#region src/context.d.ts
87
+ interface AgentInfo {
88
+ detected: boolean;
89
+ name?: string;
90
+ via?: string;
91
+ }
92
+ declare function detectAgent(env: NodeJS.ProcessEnv, extraMarkers?: string[]): AgentInfo;
93
+ declare function detectCI(env: NodeJS.ProcessEnv): boolean;
94
+ //#endregion
95
+ //#region src/config.d.ts
96
+ type Encoding = 'base64url' | 'base64' | 'hex';
97
+ interface VarDecl {
98
+ description?: string;
99
+ required?: boolean;
100
+ /** Defaults to true: the value must come from arca. `false` = config that lives in `.env`. */
101
+ sensitive?: boolean;
102
+ example?: string;
103
+ docs?: string;
104
+ generate?: {
105
+ bytes?: number;
106
+ encoding?: Encoding;
107
+ };
108
+ }
109
+ interface ArcaConfig {
110
+ id: string;
111
+ name: string;
112
+ envFiles: string[];
113
+ leakGuard: {
114
+ ignore: string[];
115
+ };
116
+ vars: Record<string, VarDecl>;
117
+ }
118
+ //#endregion
119
+ export { type AgentInfo, type ArcaConfig, type EnvEntry, type IO, type Needle, type OutStream, type Prompts, Redactor, type Secret, VERSION, type VarDecl, createMaskedWriter, detectAgent, detectCI, main, needlesFor, parseEnv, parseEnvEntries, processIO };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { a as detectAgent, i as parseEnvEntries, n as main, o as detectCI, r as parseEnv, s as processIO, t as VERSION } from "./main-Cy_OrS0c.mjs";
2
+ import { n as createMaskedWriter, r as needlesFor, t as Redactor } from "./redact-D4Txfinc.mjs";
3
+ export { Redactor, VERSION, createMaskedWriter, detectAgent, detectCI, main, needlesFor, parseEnv, parseEnvEntries, processIO };