@enactprotocol/secrets 2.0.0

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.
@@ -0,0 +1,352 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ createEnvContent,
4
+ parseEnvContent,
5
+ parseEnvFile,
6
+ removeEnvVar,
7
+ serializeEnvFile,
8
+ updateEnvVar,
9
+ } from "../../src/env/parser";
10
+
11
+ describe("env/parser", () => {
12
+ describe("parseEnvContent", () => {
13
+ it("should parse simple key=value pairs", () => {
14
+ const content = `FOO=bar
15
+ BAZ=qux`;
16
+ const result = parseEnvContent(content);
17
+
18
+ expect(result.FOO).toBe("bar");
19
+ expect(result.BAZ).toBe("qux");
20
+ });
21
+
22
+ it("should ignore comments", () => {
23
+ const content = `# This is a comment
24
+ FOO=bar
25
+ # Another comment
26
+ BAZ=qux`;
27
+ const result = parseEnvContent(content);
28
+
29
+ expect(Object.keys(result)).toHaveLength(2);
30
+ expect(result.FOO).toBe("bar");
31
+ expect(result.BAZ).toBe("qux");
32
+ });
33
+
34
+ it("should ignore empty lines", () => {
35
+ const content = `FOO=bar
36
+
37
+ BAZ=qux
38
+
39
+ `;
40
+ const result = parseEnvContent(content);
41
+
42
+ expect(Object.keys(result)).toHaveLength(2);
43
+ });
44
+
45
+ it("should handle values with = signs", () => {
46
+ const content = "DATABASE_URL=postgres://user:pass@host:5432/db";
47
+ const result = parseEnvContent(content);
48
+
49
+ expect(result.DATABASE_URL).toBe("postgres://user:pass@host:5432/db");
50
+ });
51
+
52
+ it("should handle double-quoted values", () => {
53
+ const content = `MESSAGE="Hello World"`;
54
+ const result = parseEnvContent(content);
55
+
56
+ expect(result.MESSAGE).toBe("Hello World");
57
+ });
58
+
59
+ it("should handle single-quoted values", () => {
60
+ const content = `MESSAGE='Hello World'`;
61
+ const result = parseEnvContent(content);
62
+
63
+ expect(result.MESSAGE).toBe("Hello World");
64
+ });
65
+
66
+ it("should handle inline comments for unquoted values", () => {
67
+ const content = "FOO=bar # this is a comment";
68
+ const result = parseEnvContent(content);
69
+
70
+ expect(result.FOO).toBe("bar");
71
+ });
72
+
73
+ it("should not treat # in quoted values as comments", () => {
74
+ const content = `MESSAGE="Hello #World"`;
75
+ const result = parseEnvContent(content);
76
+
77
+ expect(result.MESSAGE).toBe("Hello #World");
78
+ });
79
+
80
+ it("should handle escape sequences in double quotes", () => {
81
+ const content = `MESSAGE="Line1\\nLine2"`;
82
+ const result = parseEnvContent(content);
83
+
84
+ expect(result.MESSAGE).toBe("Line1\nLine2");
85
+ });
86
+
87
+ it("should handle escaped quotes in double-quoted values", () => {
88
+ const content = `MESSAGE="Say \\"Hello\\""`;
89
+ const result = parseEnvContent(content);
90
+
91
+ expect(result.MESSAGE).toBe('Say "Hello"');
92
+ });
93
+
94
+ it("should handle empty values", () => {
95
+ const content = `EMPTY=
96
+ FOO=bar`;
97
+ const result = parseEnvContent(content);
98
+
99
+ expect(result.EMPTY).toBe("");
100
+ expect(result.FOO).toBe("bar");
101
+ });
102
+
103
+ it("should handle values with spaces (unquoted)", () => {
104
+ const content = "MESSAGE=Hello World";
105
+ const result = parseEnvContent(content);
106
+
107
+ // Unquoted values may include spaces until inline comment
108
+ expect(result.MESSAGE).toBe("Hello World");
109
+ });
110
+
111
+ it("should treat lines without = as invalid", () => {
112
+ const content = `VALID=value
113
+ INVALID_LINE
114
+ ANOTHER=valid`;
115
+ const result = parseEnvContent(content);
116
+
117
+ expect(Object.keys(result)).toHaveLength(2);
118
+ expect(result.VALID).toBe("value");
119
+ expect(result.ANOTHER).toBe("valid");
120
+ });
121
+ });
122
+
123
+ describe("parseEnvFile", () => {
124
+ it("should preserve original line structure", () => {
125
+ const content = `# Header comment
126
+ FOO=bar
127
+
128
+ # Section
129
+ BAZ=qux`;
130
+ const result = parseEnvFile(content);
131
+
132
+ expect(result.lines).toHaveLength(5);
133
+ expect(result.lines[0]?.type).toBe("comment");
134
+ expect(result.lines[1]?.type).toBe("variable");
135
+ expect(result.lines[2]?.type).toBe("empty");
136
+ expect(result.lines[3]?.type).toBe("comment");
137
+ expect(result.lines[4]?.type).toBe("variable");
138
+ });
139
+
140
+ it("should store raw line content", () => {
141
+ const content = "FOO=bar";
142
+ const result = parseEnvFile(content);
143
+
144
+ expect(result.lines[0]?.raw).toBe("FOO=bar");
145
+ });
146
+
147
+ it("should return both vars and lines", () => {
148
+ const content = `FOO=bar
149
+ BAZ=qux`;
150
+ const result = parseEnvFile(content);
151
+
152
+ expect(result.vars.FOO).toBe("bar");
153
+ expect(result.vars.BAZ).toBe("qux");
154
+ expect(result.lines).toHaveLength(2);
155
+ });
156
+ });
157
+
158
+ describe("serializeEnvFile", () => {
159
+ it("should serialize a parsed env file back to string", () => {
160
+ const content = `FOO=bar
161
+ BAZ=qux`;
162
+ const parsed = parseEnvFile(content);
163
+ const serialized = serializeEnvFile(parsed);
164
+
165
+ expect(serialized).toBe(content);
166
+ });
167
+
168
+ it("should preserve comments", () => {
169
+ const content = `# Comment
170
+ FOO=bar`;
171
+ const parsed = parseEnvFile(content);
172
+ const serialized = serializeEnvFile(parsed);
173
+
174
+ expect(serialized).toBe(content);
175
+ });
176
+
177
+ it("should preserve empty lines", () => {
178
+ const content = `FOO=bar
179
+
180
+ BAZ=qux`;
181
+ const parsed = parseEnvFile(content);
182
+ const serialized = serializeEnvFile(parsed);
183
+
184
+ expect(serialized).toBe(content);
185
+ });
186
+
187
+ it("should quote values with spaces", () => {
188
+ const parsed = parseEnvFile("FOO=bar");
189
+ parsed.vars.FOO = "hello world";
190
+
191
+ const serialized = serializeEnvFile(parsed);
192
+ expect(serialized).toBe('FOO="hello world"');
193
+ });
194
+
195
+ it("should quote values with = signs", () => {
196
+ const parsed = parseEnvFile("URL=test");
197
+ parsed.vars.URL = "host=localhost";
198
+
199
+ const serialized = serializeEnvFile(parsed);
200
+ expect(serialized).toBe('URL="host=localhost"');
201
+ });
202
+
203
+ it("should escape quotes in quoted values", () => {
204
+ const parsed = parseEnvFile("MSG=test");
205
+ parsed.vars.MSG = 'Say "Hello"';
206
+
207
+ const serialized = serializeEnvFile(parsed);
208
+ expect(serialized).toBe('MSG="Say \\"Hello\\""');
209
+ });
210
+ });
211
+
212
+ describe("createEnvContent", () => {
213
+ it("should create env content from object", () => {
214
+ const vars = {
215
+ FOO: "bar",
216
+ BAZ: "qux",
217
+ };
218
+ const result = createEnvContent(vars);
219
+
220
+ expect(result).toContain("FOO=bar");
221
+ expect(result).toContain("BAZ=qux");
222
+ });
223
+
224
+ it("should quote values with special characters", () => {
225
+ const vars = {
226
+ MESSAGE: "Hello World",
227
+ };
228
+ const result = createEnvContent(vars);
229
+
230
+ expect(result).toBe('MESSAGE="Hello World"');
231
+ });
232
+
233
+ it("should handle empty object", () => {
234
+ const result = createEnvContent({});
235
+ expect(result).toBe("");
236
+ });
237
+ });
238
+
239
+ describe("updateEnvVar", () => {
240
+ it("should update existing variable", () => {
241
+ const content = `FOO=old
242
+ BAZ=qux`;
243
+ const parsed = parseEnvFile(content);
244
+ const updated = updateEnvVar(parsed, "FOO", "new");
245
+
246
+ expect(updated.vars.FOO).toBe("new");
247
+ expect(updated.lines).toHaveLength(2);
248
+ });
249
+
250
+ it("should add new variable if not exists", () => {
251
+ const content = "FOO=bar";
252
+ const parsed = parseEnvFile(content);
253
+ const updated = updateEnvVar(parsed, "NEW_VAR", "new-value");
254
+
255
+ expect(updated.vars.NEW_VAR).toBe("new-value");
256
+ expect(updated.lines).toHaveLength(2);
257
+ });
258
+
259
+ it("should preserve other lines", () => {
260
+ const content = `# Comment
261
+ FOO=bar
262
+
263
+ BAZ=qux`;
264
+ const parsed = parseEnvFile(content);
265
+ const updated = updateEnvVar(parsed, "FOO", "new");
266
+
267
+ expect(updated.lines).toHaveLength(4);
268
+ expect(updated.lines[0]?.type).toBe("comment");
269
+ expect(updated.lines[2]?.type).toBe("empty");
270
+ });
271
+
272
+ it("should update line's raw content", () => {
273
+ const content = "FOO=old";
274
+ const parsed = parseEnvFile(content);
275
+ const updated = updateEnvVar(parsed, "FOO", "new");
276
+ const line = updated.lines[0];
277
+
278
+ expect(line?.raw).toBe("FOO=new");
279
+ });
280
+ });
281
+
282
+ describe("removeEnvVar", () => {
283
+ it("should remove existing variable", () => {
284
+ const content = `FOO=bar
285
+ BAZ=qux`;
286
+ const parsed = parseEnvFile(content);
287
+ const updated = removeEnvVar(parsed, "FOO");
288
+
289
+ expect(updated.vars.FOO).toBeUndefined();
290
+ expect(updated.vars.BAZ).toBe("qux");
291
+ expect(updated.lines).toHaveLength(1);
292
+ });
293
+
294
+ it("should preserve comments and empty lines", () => {
295
+ const content = `# Comment
296
+ FOO=bar
297
+
298
+ BAZ=qux`;
299
+ const parsed = parseEnvFile(content);
300
+ const updated = removeEnvVar(parsed, "FOO");
301
+
302
+ expect(updated.lines).toHaveLength(3);
303
+ expect(updated.lines[0]?.type).toBe("comment");
304
+ expect(updated.lines[1]?.type).toBe("empty");
305
+ expect(updated.lines[2]?.type).toBe("variable");
306
+ });
307
+
308
+ it("should return same object if key not found", () => {
309
+ const content = "FOO=bar";
310
+ const parsed = parseEnvFile(content);
311
+ const updated = removeEnvVar(parsed, "NONEXISTENT");
312
+
313
+ expect(Object.keys(updated.vars)).toHaveLength(1);
314
+ expect(updated.vars.FOO).toBe("bar");
315
+ });
316
+ });
317
+
318
+ describe("complex .env files", () => {
319
+ it("should parse complex fixture", () => {
320
+ const content = `# Database configuration
321
+ DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
322
+ DATABASE_POOL_SIZE=10
323
+
324
+ # API Keys
325
+ STRIPE_API_KEY=sk_test_12345 # development key
326
+ STRIPE_WEBHOOK_SECRET="whsec_abc123"
327
+
328
+ # Feature Flags
329
+ ENABLE_NEW_FEATURE=true
330
+ DEPRECATED_FEATURE=false
331
+
332
+ # Multiline-like content
333
+ JSON_CONFIG='{"key": "value"}'
334
+ MESSAGE="Line 1\\nLine 2"`;
335
+
336
+ const result = parseEnvFile(content);
337
+
338
+ expect(result.vars.DATABASE_URL).toBe("postgres://user:pass@localhost:5432/mydb");
339
+ expect(result.vars.DATABASE_POOL_SIZE).toBe("10");
340
+ expect(result.vars.STRIPE_API_KEY).toBe("sk_test_12345");
341
+ expect(result.vars.STRIPE_WEBHOOK_SECRET).toBe("whsec_abc123");
342
+ expect(result.vars.ENABLE_NEW_FEATURE).toBe("true");
343
+ expect(result.vars.JSON_CONFIG).toBe('{"key": "value"}');
344
+ expect(result.vars.MESSAGE).toBe("Line 1\nLine 2");
345
+
346
+ // Check structure is preserved
347
+ expect(result.lines.filter((l) => l.type === "comment")).toHaveLength(4);
348
+ expect(result.lines.filter((l) => l.type === "empty")).toHaveLength(3);
349
+ expect(result.lines.filter((l) => l.type === "variable")).toHaveLength(8);
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,257 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ getGlobalEnvPath,
7
+ getLocalEnvPath,
8
+ loadLocalEnv,
9
+ localEnvExists,
10
+ readEnvFile,
11
+ readEnvVars,
12
+ } from "../../src/env/reader";
13
+ import {
14
+ deleteEnvVar,
15
+ deleteLocalEnvVar,
16
+ setEnvVar,
17
+ setLocalEnvVar,
18
+ writeEnvFile,
19
+ writeEnvVars,
20
+ } from "../../src/env/writer";
21
+
22
+ describe("env/reader", () => {
23
+ const testDir = join(tmpdir(), `enact-test-${Date.now()}`);
24
+ const testEnvPath = join(testDir, ".env");
25
+
26
+ beforeEach(() => {
27
+ mkdirSync(testDir, { recursive: true });
28
+ });
29
+
30
+ afterEach(() => {
31
+ if (existsSync(testDir)) {
32
+ rmSync(testDir, { recursive: true });
33
+ }
34
+ });
35
+
36
+ describe("getGlobalEnvPath", () => {
37
+ it("should return path to ~/.enact/.env", () => {
38
+ const path = getGlobalEnvPath();
39
+ expect(path).toContain(".enact");
40
+ expect(path).toContain(".env");
41
+ });
42
+ });
43
+
44
+ describe("getLocalEnvPath", () => {
45
+ it("should return path relative to cwd", () => {
46
+ const path = getLocalEnvPath(testDir);
47
+ expect(path).toBe(join(testDir, ".enact", ".env"));
48
+ });
49
+ });
50
+
51
+ describe("readEnvFile", () => {
52
+ it("should read and parse an existing .env file", () => {
53
+ writeFileSync(testEnvPath, "FOO=bar\nBAZ=qux");
54
+
55
+ const result = readEnvFile(testEnvPath);
56
+
57
+ expect(result.vars.FOO).toBe("bar");
58
+ expect(result.vars.BAZ).toBe("qux");
59
+ expect(result.lines).toHaveLength(2);
60
+ });
61
+
62
+ it("should return empty object for non-existent file", () => {
63
+ const result = readEnvFile(join(testDir, "nonexistent.env"));
64
+
65
+ expect(result.vars).toEqual({});
66
+ expect(result.lines).toEqual([]);
67
+ });
68
+ });
69
+
70
+ describe("readEnvVars", () => {
71
+ it("should read .env file as key-value object", () => {
72
+ writeFileSync(testEnvPath, "FOO=bar\nBAZ=qux");
73
+
74
+ const result = readEnvVars(testEnvPath);
75
+
76
+ expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
77
+ });
78
+
79
+ it("should return empty object for non-existent file", () => {
80
+ const result = readEnvVars(join(testDir, "nonexistent.env"));
81
+ expect(result).toEqual({});
82
+ });
83
+ });
84
+
85
+ describe("loadLocalEnv", () => {
86
+ it("should load local .env file", () => {
87
+ const localDir = join(testDir, ".enact");
88
+ mkdirSync(localDir, { recursive: true });
89
+ writeFileSync(join(localDir, ".env"), "LOCAL_KEY=local-value");
90
+
91
+ const result = loadLocalEnv(testDir);
92
+
93
+ expect(result.LOCAL_KEY).toBe("local-value");
94
+ });
95
+
96
+ it("should return empty object if local .env doesn't exist", () => {
97
+ const result = loadLocalEnv(testDir);
98
+ expect(result).toEqual({});
99
+ });
100
+ });
101
+
102
+ describe("localEnvExists", () => {
103
+ it("should return true if local .env exists", () => {
104
+ const localDir = join(testDir, ".enact");
105
+ mkdirSync(localDir, { recursive: true });
106
+ writeFileSync(join(localDir, ".env"), "KEY=value");
107
+
108
+ expect(localEnvExists(testDir)).toBe(true);
109
+ });
110
+
111
+ it("should return false if local .env doesn't exist", () => {
112
+ expect(localEnvExists(testDir)).toBe(false);
113
+ });
114
+ });
115
+ });
116
+
117
+ describe("env/writer", () => {
118
+ const testDir = join(tmpdir(), `enact-test-writer-${Date.now()}`);
119
+
120
+ beforeEach(() => {
121
+ mkdirSync(testDir, { recursive: true });
122
+ });
123
+
124
+ afterEach(() => {
125
+ if (existsSync(testDir)) {
126
+ rmSync(testDir, { recursive: true });
127
+ }
128
+ });
129
+
130
+ describe("writeEnvFile", () => {
131
+ it("should write parsed env file to disk", () => {
132
+ const envPath = join(testDir, "test.env");
133
+ const parsed = {
134
+ vars: { FOO: "bar", BAZ: "qux" },
135
+ lines: [
136
+ { type: "variable" as const, raw: "FOO=bar", key: "FOO", value: "bar" },
137
+ { type: "variable" as const, raw: "BAZ=qux", key: "BAZ", value: "qux" },
138
+ ],
139
+ };
140
+
141
+ writeEnvFile(envPath, parsed);
142
+ const result = readEnvVars(envPath);
143
+
144
+ expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
145
+ });
146
+
147
+ it("should create parent directories if needed", () => {
148
+ const envPath = join(testDir, "nested", "deep", ".env");
149
+
150
+ writeEnvFile(envPath, { vars: { KEY: "value" }, lines: [] });
151
+
152
+ expect(existsSync(join(testDir, "nested", "deep"))).toBe(true);
153
+ });
154
+ });
155
+
156
+ describe("writeEnvVars", () => {
157
+ it("should write key-value object to .env file", () => {
158
+ const envPath = join(testDir, "test.env");
159
+
160
+ writeEnvVars(envPath, { FOO: "bar", BAZ: "qux" });
161
+ const result = readEnvVars(envPath);
162
+
163
+ expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
164
+ });
165
+ });
166
+
167
+ describe("setEnvVar", () => {
168
+ it("should add a new variable to existing file", () => {
169
+ const envPath = join(testDir, "test.env");
170
+ writeFileSync(envPath, "FOO=bar");
171
+
172
+ setEnvVar(envPath, "BAZ", "qux");
173
+ const result = readEnvVars(envPath);
174
+
175
+ expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
176
+ });
177
+
178
+ it("should update an existing variable", () => {
179
+ const envPath = join(testDir, "test.env");
180
+ writeFileSync(envPath, "FOO=old");
181
+
182
+ setEnvVar(envPath, "FOO", "new");
183
+ const result = readEnvVars(envPath);
184
+
185
+ expect(result.FOO).toBe("new");
186
+ });
187
+
188
+ it("should create file if it doesn't exist", () => {
189
+ const envPath = join(testDir, "newfile.env");
190
+
191
+ setEnvVar(envPath, "KEY", "value");
192
+ const result = readEnvVars(envPath);
193
+
194
+ expect(result.KEY).toBe("value");
195
+ });
196
+
197
+ it("should preserve comments and formatting", () => {
198
+ const envPath = join(testDir, "test.env");
199
+ writeFileSync(envPath, "# Comment\nFOO=bar\n\nBAZ=qux");
200
+
201
+ setEnvVar(envPath, "FOO", "updated");
202
+ const parsed = readEnvFile(envPath);
203
+
204
+ expect(parsed.lines[0]?.type).toBe("comment");
205
+ expect(parsed.lines[2]?.type).toBe("empty");
206
+ expect(parsed.vars.FOO).toBe("updated");
207
+ });
208
+ });
209
+
210
+ describe("deleteEnvVar", () => {
211
+ it("should delete an existing variable", () => {
212
+ const envPath = join(testDir, "test.env");
213
+ writeFileSync(envPath, "FOO=bar\nBAZ=qux");
214
+
215
+ const deleted = deleteEnvVar(envPath, "FOO");
216
+ const result = readEnvVars(envPath);
217
+
218
+ expect(deleted).toBe(true);
219
+ expect(result.FOO).toBeUndefined();
220
+ expect(result.BAZ).toBe("qux");
221
+ });
222
+
223
+ it("should return false for non-existent variable", () => {
224
+ const envPath = join(testDir, "test.env");
225
+ writeFileSync(envPath, "FOO=bar");
226
+
227
+ const deleted = deleteEnvVar(envPath, "NONEXISTENT");
228
+
229
+ expect(deleted).toBe(false);
230
+ });
231
+ });
232
+
233
+ describe("setLocalEnvVar", () => {
234
+ it("should set a variable in local .env", () => {
235
+ setLocalEnvVar("LOCAL_KEY", "local-value", testDir);
236
+ const result = loadLocalEnv(testDir);
237
+
238
+ expect(result.LOCAL_KEY).toBe("local-value");
239
+ });
240
+
241
+ it("should create .enact directory if needed", () => {
242
+ setLocalEnvVar("KEY", "value", testDir);
243
+
244
+ expect(existsSync(join(testDir, ".enact"))).toBe(true);
245
+ });
246
+ });
247
+
248
+ describe("deleteLocalEnvVar", () => {
249
+ it("should delete a variable from local .env", () => {
250
+ setLocalEnvVar("KEY", "value", testDir);
251
+ const deleted = deleteLocalEnvVar("KEY", testDir);
252
+
253
+ expect(deleted).toBe(true);
254
+ expect(loadLocalEnv(testDir).KEY).toBeUndefined();
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,26 @@
1
+ # Complex .env file with various formats
2
+
3
+ # Quoted values
4
+ SINGLE_QUOTED='value with spaces'
5
+ DOUBLE_QUOTED="another value with spaces"
6
+
7
+ # Escaped sequences in double quotes
8
+ ESCAPED_NEWLINE="line1\nline2"
9
+ ESCAPED_TAB="col1\tcol2"
10
+
11
+ # Values with special characters
12
+ URL_WITH_PARAMS=https://api.example.com?key=value&other=123
13
+ PATH_VALUE=/usr/local/bin:/home/user/bin
14
+
15
+ # Inline comments
16
+ SETTING=value # this is a comment
17
+
18
+ # Empty and whitespace
19
+ EMPTY_VALUE=
20
+ SPACES_AROUND = trimmed
21
+
22
+ # Values with equals sign
23
+ CONNECTION_STRING=host=localhost;port=5432;db=mydb
24
+
25
+ # Multi-word unquoted (should work)
26
+ SIMPLE_TEXT=hello world
File without changes
@@ -0,0 +1,4 @@
1
+ # Simple .env file
2
+ LOG_LEVEL=debug
3
+ API_BASE_URL=https://api.example.com
4
+ TIMEOUT=30