kryptheon 0.1.5 → 0.1.7

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,191 @@
1
+ // Which environment variables a recording needs, and whether they are there.
2
+ //
3
+ // Why this exists. When a password is typed during recording it is taken out
4
+ // of the spec and replaced with `process.env.KRYPTHEON_PASSWORD || ''`. If
5
+ // nobody ever creates the .env file, that expression quietly evaluates to an
6
+ // empty string: the run submits a blank password, the login fails for the most
7
+ // boring reason imaginable, and the report blames the app. Two hours can go
8
+ // into that before anyone thinks to check the environment.
9
+ //
10
+ // So the names are read out of the spec before anything runs, and a recording
11
+ // whose secret is missing is not run at all - a blank password is never sent.
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // The exact shape the scrubber writes. Anything else in the file that happens
17
+ // to mention process.env is the author's own business and left alone.
18
+ const SECRET_USE = /process\.env\.([A-Z][A-Z0-9_]*)\s*\|\|\s*(['"])\2/g;
19
+
20
+ /** Every environment variable this recording falls back to an empty string for. */
21
+ function requiredSecrets(source) {
22
+ const out = [];
23
+ const seen = Object.create(null);
24
+ let match;
25
+ SECRET_USE.lastIndex = 0;
26
+ while ((match = SECRET_USE.exec(String(source || '')))) {
27
+ const name = match[1];
28
+ if (seen[name]) continue;
29
+ seen[name] = true;
30
+ out.push(name);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Reads a .env file into a plain object. Never throws: a missing file simply
37
+ * means nothing was defined there.
38
+ *
39
+ * Values are not expanded or unescaped beyond stripping one layer of quotes -
40
+ * this only has to answer "is it set", and guessing at more would be a second
41
+ * dialect of .env for people to trip over.
42
+ */
43
+ function readEnvFile(file) {
44
+ const out = Object.create(null);
45
+ let raw;
46
+ try {
47
+ raw = fs.readFileSync(file, 'utf8');
48
+ } catch (e) {
49
+ return out;
50
+ }
51
+ // A byte order mark would otherwise become part of the first name.
52
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
53
+ for (const line of raw.split('\n')) {
54
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
55
+ if (!match) continue;
56
+ out[match[1]] = match[2].replace(/^(['"])([\s\S]*)\1$/, '$2');
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /** A value that is present and not blank. An empty variable is not set. */
62
+ function isSet(value) {
63
+ return typeof value === 'string' && value.trim() !== '';
64
+ }
65
+
66
+ /**
67
+ * Which of the names a recording needs are missing, looking at both the real
68
+ * environment and the project's .env - the same two places the test itself
69
+ * will look when it runs.
70
+ */
71
+ function missingSecrets(names, options) {
72
+ const opts = options || {};
73
+ const env = opts.env || process.env;
74
+ const fromFile = opts.envFile ? readEnvFile(opts.envFile) : {};
75
+ return (names || []).filter((name) => !isSet(env[name]) && !isSet(fromFile[name]));
76
+ }
77
+
78
+ /**
79
+ * How to create the file without a byte order mark.
80
+ *
81
+ * PowerShell's own `Out-File -Encoding utf8` writes one, and so does Notepad,
82
+ * which is why this spells out a command that does not rather than leaving it
83
+ * to whatever the person reaches for first.
84
+ */
85
+ function howToWriteEnv(names, platform) {
86
+ const line = names.map((name) => name + '=yourpassword').join('\\n');
87
+ if ((platform || process.platform) === 'win32') {
88
+ return [
89
+ ' In PowerShell, this writes it without a byte order mark:',
90
+ ' [IO.File]::WriteAllText("$PWD\\.env", "' + line + '`n")',
91
+ '',
92
+ ' Avoid Out-File and Notepad here: both add a mark that makes the first',
93
+ ' line unreadable, and the password would silently come out blank.',
94
+ ];
95
+ }
96
+ return [' For example:', " printf '" + line.replace(/\\n/g, "\\n") + "\\n' > .env"];
97
+ }
98
+
99
+ /** What to tell somebody who has not created the file yet. */
100
+ function missingSecretLines(specName, names, options) {
101
+ const opts = options || {};
102
+ const many = names.length > 1;
103
+ const lines = [
104
+ ' Skipped ' + specName,
105
+ ' This recording needs a password' + (many ? ' and other secrets' : '') + ', and there is none set.',
106
+ '',
107
+ ];
108
+ if (opts.examplePath) {
109
+ lines.push(' There is a ' + opts.examplePath + ' here already. Put the real value in it');
110
+ lines.push(' and save it as .env, in the same folder.');
111
+ } else {
112
+ lines.push(' Create a .env file in this folder containing:');
113
+ for (const name of names) lines.push(' ' + name + '=yourpassword');
114
+ }
115
+ lines.push('');
116
+ for (const line of howToWriteEnv(names, opts.platform)) lines.push(line);
117
+ lines.push('');
118
+ lines.push(' Without it the recording would sign in with a blank password and');
119
+ lines.push(' fail for a reason that has nothing to do with your app, so it was');
120
+ lines.push(' not run. Keep .env out of version control.');
121
+ return lines;
122
+ }
123
+
124
+ /**
125
+ * Writes .env.example so nobody has to get the encoding right by hand.
126
+ *
127
+ * Plain UTF-8, no byte order mark. Never overwrites: the file may already hold
128
+ * names from an earlier recording, and clobbering it would lose them.
129
+ */
130
+ function writeEnvExample(dir, names) {
131
+ const file = path.join(dir, '.env.example');
132
+ if (fs.existsSync(file)) return null;
133
+ const body = (names || []).map((name) => name + '=yourpassword').join('\n') + '\n';
134
+ try {
135
+ fs.writeFileSync(file, body, { encoding: 'utf8' });
136
+ return file;
137
+ } catch (e) {
138
+ return null; // a convenience, never a reason to fail a recording
139
+ }
140
+ }
141
+
142
+ // Everything kryptheon leaves in a project that should not be committed: the
143
+ // real password, and three files that are machine state rather than source.
144
+ const GITIGNORE_ENTRIES = ['.env', 'kryptheon-baselines.json', 'kryptheon-history.jsonl', 'test-results/'];
145
+
146
+ /**
147
+ * Adds the missing entries to an existing .gitignore. Returns what it added.
148
+ *
149
+ * Only touches a file that is already there - creating one in a project that
150
+ * has deliberately gone without is not this tool's decision to make.
151
+ */
152
+ function updateGitignore(dir, entries) {
153
+ const file = path.join(dir, '.gitignore');
154
+ let raw;
155
+ try {
156
+ raw = fs.readFileSync(file, { encoding: 'utf8' });
157
+ } catch (e) {
158
+ return [];
159
+ }
160
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
161
+ const present = new Set(
162
+ raw
163
+ .split('\n')
164
+ .map((line) => line.trim().replace(/^\/+/, '').replace(/\/+$/, ''))
165
+ .filter(Boolean),
166
+ );
167
+ const wanted = entries || GITIGNORE_ENTRIES;
168
+ const missing = wanted.filter((entry) => !present.has(entry.replace(/\/+$/, '')));
169
+ if (!missing.length) return [];
170
+ const needsNewline = raw.length && !raw.endsWith('\n');
171
+ const block = (needsNewline ? '\n' : '') + '\n# kryptheon\n' + missing.join('\n') + '\n';
172
+ try {
173
+ fs.appendFileSync(file, block, { encoding: 'utf8' });
174
+ return missing;
175
+ } catch (e) {
176
+ return [];
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ requiredSecrets: requiredSecrets,
182
+ readEnvFile: readEnvFile,
183
+ missingSecrets: missingSecrets,
184
+ missingSecretLines: missingSecretLines,
185
+ howToWriteEnv: howToWriteEnv,
186
+ writeEnvExample: writeEnvExample,
187
+ updateGitignore: updateGitignore,
188
+ GITIGNORE_ENTRIES: GITIGNORE_ENTRIES,
189
+ isSet: isSet,
190
+ envFileFor: (dir) => path.join(dir, '.env'),
191
+ };