@bremmdev/arca-env 0.0.0-stage → 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/README.md +110 -2
- package/dist/cli.mjs +7 -0
- package/dist/guard.mjs +90 -0
- package/dist/index.d.mts +119 -0
- package/dist/index.mjs +3 -0
- package/dist/main-Cy_OrS0c.mjs +2783 -0
- package/dist/redact-D4Txfinc.mjs +151 -0
- package/package.json +51 -4
- package/schema.json +63 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
|
+
/** Builds needles for a value: raw, URL-encoded, JSON-escaped, base64, base64url and URL credentials. */
|
|
3
|
+
function needlesFor(secret) {
|
|
4
|
+
const { name, value } = secret;
|
|
5
|
+
const out = [];
|
|
6
|
+
const add = (text, replacement = `[arca:${name}]`) => {
|
|
7
|
+
if (text.length >= 6) out.push({
|
|
8
|
+
text,
|
|
9
|
+
replacement
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
if (value.length < 6) return out;
|
|
13
|
+
add(value);
|
|
14
|
+
add(encodeURIComponent(value));
|
|
15
|
+
add(JSON.stringify(value).slice(1, -1));
|
|
16
|
+
add(Buffer.from(value, "utf8").toString("base64"));
|
|
17
|
+
add(Buffer.from(value, "utf8").toString("base64url"));
|
|
18
|
+
const url = parseCredentialUrl(value);
|
|
19
|
+
if (url) {
|
|
20
|
+
add(`${url.rawUser}:${url.rawPassword}`, `[arca:${name}#credentials]`);
|
|
21
|
+
add(url.rawPassword, `[arca:${name}#password]`);
|
|
22
|
+
if (url.password !== url.rawPassword) add(url.password, `[arca:${name}#password]`);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function parseCredentialUrl(value) {
|
|
27
|
+
const m = /^[a-z][a-z0-9+.-]*:\/\/([^:@/\s]*):([^@/\s]+)@/i.exec(value);
|
|
28
|
+
if (!m) return null;
|
|
29
|
+
const rawPassword = m[2];
|
|
30
|
+
let password = rawPassword;
|
|
31
|
+
try {
|
|
32
|
+
password = decodeURIComponent(rawPassword);
|
|
33
|
+
} catch {}
|
|
34
|
+
return {
|
|
35
|
+
rawUser: m[1],
|
|
36
|
+
rawPassword,
|
|
37
|
+
password
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var Redactor = class {
|
|
41
|
+
byFirst = /* @__PURE__ */ new Map();
|
|
42
|
+
held = "";
|
|
43
|
+
size;
|
|
44
|
+
constructor(needles) {
|
|
45
|
+
const unique = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const n of needles) {
|
|
47
|
+
const existing = unique.get(n.text);
|
|
48
|
+
if (!existing || existing.replacement.includes("#") && !n.replacement.includes("#")) unique.set(n.text, n);
|
|
49
|
+
}
|
|
50
|
+
const sorted = [...unique.values()].sort((a, b) => b.text.length - a.text.length);
|
|
51
|
+
for (const n of sorted) {
|
|
52
|
+
const c = n.text[0];
|
|
53
|
+
const list = this.byFirst.get(c) ?? [];
|
|
54
|
+
list.push(n);
|
|
55
|
+
this.byFirst.set(c, list);
|
|
56
|
+
}
|
|
57
|
+
this.size = unique.size;
|
|
58
|
+
}
|
|
59
|
+
/** Masks a complete string (no streaming). */
|
|
60
|
+
redact(text) {
|
|
61
|
+
return this.scan(text, true).out;
|
|
62
|
+
}
|
|
63
|
+
/** Feeds a chunk; returns what can be printed now. */
|
|
64
|
+
push(chunk) {
|
|
65
|
+
const { out, hold } = this.scan(this.held + chunk, false);
|
|
66
|
+
this.held = hold;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** Returns the held tail, masked as far as possible. */
|
|
70
|
+
flush() {
|
|
71
|
+
const { out } = this.scan(this.held, true);
|
|
72
|
+
this.held = "";
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
get pending() {
|
|
76
|
+
return this.held.length > 0;
|
|
77
|
+
}
|
|
78
|
+
scan(text, final) {
|
|
79
|
+
if (this.size === 0) return {
|
|
80
|
+
out: text,
|
|
81
|
+
hold: ""
|
|
82
|
+
};
|
|
83
|
+
let out = "";
|
|
84
|
+
let i = 0;
|
|
85
|
+
let plainStart = 0;
|
|
86
|
+
while (i < text.length) {
|
|
87
|
+
const candidates = this.byFirst.get(text[i]);
|
|
88
|
+
if (candidates) {
|
|
89
|
+
let match = null;
|
|
90
|
+
let partial = false;
|
|
91
|
+
for (const n of candidates) if (i + n.text.length <= text.length) {
|
|
92
|
+
if (text.startsWith(n.text, i)) {
|
|
93
|
+
match = n;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
} else if (!final && n.text.startsWith(text.slice(i))) partial = true;
|
|
97
|
+
if (partial) {
|
|
98
|
+
out += text.slice(plainStart, i);
|
|
99
|
+
return {
|
|
100
|
+
out,
|
|
101
|
+
hold: text.slice(i)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (match) {
|
|
105
|
+
out += text.slice(plainStart, i) + match.replacement;
|
|
106
|
+
i += match.text.length;
|
|
107
|
+
plainStart = i;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
out += text.slice(plainStart);
|
|
114
|
+
return {
|
|
115
|
+
out,
|
|
116
|
+
hold: ""
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/** Wires a byte stream through a Redactor into `write`, with the 50 ms hold-back flush. */
|
|
121
|
+
function createMaskedWriter(redactor, write, flushMs = 50) {
|
|
122
|
+
const decoder = new StringDecoder("utf8");
|
|
123
|
+
let timer = null;
|
|
124
|
+
const schedule = () => {
|
|
125
|
+
if (timer) clearTimeout(timer);
|
|
126
|
+
timer = null;
|
|
127
|
+
if (redactor.pending) {
|
|
128
|
+
timer = setTimeout(() => {
|
|
129
|
+
timer = null;
|
|
130
|
+
const s = redactor.flush();
|
|
131
|
+
if (s) write(s);
|
|
132
|
+
}, flushMs);
|
|
133
|
+
timer.unref?.();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
write(chunk) {
|
|
138
|
+
const s = redactor.push(typeof chunk === "string" ? chunk : decoder.write(chunk));
|
|
139
|
+
if (s) write(s);
|
|
140
|
+
schedule();
|
|
141
|
+
},
|
|
142
|
+
end() {
|
|
143
|
+
if (timer) clearTimeout(timer);
|
|
144
|
+
timer = null;
|
|
145
|
+
const s = redactor.push(decoder.end()) + redactor.flush();
|
|
146
|
+
if (s) write(s);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { createMaskedWriter as n, needlesFor as r, Redactor as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bremmdev/arca-env",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Keep secrets out of .env files and out of reach of AI agents. Encrypted local store, injected at runtime, output always masked.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "bremmdev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/bremmdev/arca-env"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"arca": "./dist/cli.mjs"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/index.mjs",
|
|
20
|
+
"./schema.json": "./schema.json",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"schema.json"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"env",
|
|
29
|
+
"dotenv",
|
|
30
|
+
"secrets",
|
|
31
|
+
"ai-agents",
|
|
32
|
+
"keychain",
|
|
33
|
+
"redaction"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@clack/prompts": "^1.8.1",
|
|
43
|
+
"@napi-rs/keyring": "^2.1.2",
|
|
44
|
+
"cross-spawn": "^7.0.6"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/cross-spawn": "^6.0.6",
|
|
48
|
+
"@types/node": "^26.6.5",
|
|
49
|
+
"tsdown": "^0.22.2",
|
|
50
|
+
"typescript": "^6.0.3",
|
|
51
|
+
"vitest": "^5.0.5"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/schema.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://unpkg.com/@bremmdev/arca-env/schema.json",
|
|
4
|
+
"title": "arca.json",
|
|
5
|
+
"description": "arca project config. Holds variable names and metadata, never values. Secrets live in arca, non-sensitive config in .env files.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^arc_[a-z2-7]{8,}$",
|
|
13
|
+
"description": "Stable project ID generated by `arca init`. Clones and git worktrees with the same ID share secrets. Don't edit by hand; use `arca init --new-id`."
|
|
14
|
+
},
|
|
15
|
+
"name": { "type": "string", "description": "Display name." },
|
|
16
|
+
"envFiles": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": { "type": "string" },
|
|
19
|
+
"default": [],
|
|
20
|
+
"description": "Opt-in: .env files arca loads itself, for tools that don't (plain node, tsx). Injected below secrets and the shell env. Frameworks load their own .env files, so leave this empty for Next.js, Vite, Astro and friends."
|
|
21
|
+
},
|
|
22
|
+
"leakGuard": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"ignore": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": { "type": "string" },
|
|
28
|
+
"description": "Variable names the leak guard should never flag in .env files."
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
},
|
|
33
|
+
"vars": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"description": "Every variable the project needs. Names only.",
|
|
36
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
37
|
+
"additionalProperties": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"description": { "type": "string", "description": "Shown in `arca setup` and `arca list`." },
|
|
41
|
+
"required": { "type": "boolean", "default": false, "description": "`arca run` and `arca check` fail when it's missing." },
|
|
42
|
+
"sensitive": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": true,
|
|
45
|
+
"description": "true (default): a secret, its value comes from arca and is masked in output. false: config, its value lives in .env."
|
|
46
|
+
},
|
|
47
|
+
"example": { "type": "string", "description": "A hint shown during `arca setup`. Never a real value." },
|
|
48
|
+
"docs": { "type": "string", "description": "Where to get the value, e.g. a dashboard URL." },
|
|
49
|
+
"generate": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"description": "Lets `arca setup` and `arca set NAME --generate` create a random value.",
|
|
52
|
+
"properties": {
|
|
53
|
+
"bytes": { "type": "integer", "minimum": 8, "maximum": 1024, "default": 32 },
|
|
54
|
+
"encoding": { "enum": ["base64url", "base64", "hex"], "default": "base64url" }
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"additionalProperties": false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|