kryptheon-night 0.1.39 → 0.1.40
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/attack.js +1028 -1025
- package/bin/kryptheon-night.js +736 -725
- package/engine.sql +4657 -4430
- package/finding.js +1715 -1612
- package/intro.js +311 -303
- package/package.json +98 -98
- package/scan.js +1330 -1311
- package/schema.js +1588 -1555
- package/tamper.js +949 -772
package/intro.js
CHANGED
|
@@ -1,303 +1,311 @@
|
|
|
1
|
-
// What the person is asked, and what they are told before anything runs.
|
|
2
|
-
//
|
|
3
|
-
// This file is the whole difference between a tool for people who write code
|
|
4
|
-
// and a tool for people who do not. The engine behind it does not change; the
|
|
5
|
-
// first ninety seconds do.
|
|
6
|
-
//
|
|
7
|
-
// Two things happen here and both are deliberate.
|
|
8
|
-
//
|
|
9
|
-
// Asking, rather than requiring. `KN_DATABASE_URL=... node scan.js public` is
|
|
10
|
-
// three unfamiliar ideas at once: an environment variable, a connection
|
|
11
|
-
// string, and a schema. Somebody who has only ever clicked Deploy has met
|
|
12
|
-
// none of them. So the command takes no arguments, and asks.
|
|
13
|
-
//
|
|
14
|
-
// Telling before doing. This tool connects to a person's production database
|
|
15
|
-
// with the owner's credential and creates things in it. Nobody should agree
|
|
16
|
-
// to that from a package name. What it does, what it does not do, and then a
|
|
17
|
-
// yes or no - in that order, before the first statement is sent.
|
|
18
|
-
|
|
19
|
-
const readline = require('readline');
|
|
20
|
-
const trouble = require('./trouble.js');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Where the connection string is, said as buttons rather than as concepts.
|
|
24
|
-
*
|
|
25
|
-
* Named screen by screen because "get your connection string" is not an
|
|
26
|
-
* instruction to somebody who does not know the phrase. Supabase first
|
|
27
|
-
* because that is who this is for; Lovable and Bolt build on Supabase, so
|
|
28
|
-
* their users end up on the same page by a different door.
|
|
29
|
-
*/
|
|
30
|
-
function whereToFindIt() {
|
|
31
|
-
// The session pooler, and said as such: the direct connection answers only
|
|
32
|
-
// over IPv6, which many home networks do not have, and the transaction
|
|
33
|
-
// pooler on 6543 cannot hold these attacks together. The kryptheon
|
|
34
|
-
// dashboard shows these same steps; kryptheon-night words hands them over.
|
|
35
|
-
return [
|
|
36
|
-
'Where to find it:',
|
|
37
|
-
'',
|
|
38
|
-
' Supabase 1. supabase.com/dashboard -> open your project.',
|
|
39
|
-
' 2. Press "Connect" at the top of the page.',
|
|
40
|
-
' 3. Choose "Session pooler" - the one on port 5432.',
|
|
41
|
-
' 4. Copy the URI. It starts postgresql://',
|
|
42
|
-
' 5. Replace [YOUR-PASSWORD] in it with your database password.',
|
|
43
|
-
' Forgotten it? Project Settings -> Database -> Reset',
|
|
44
|
-
' database password.',
|
|
45
|
-
'',
|
|
46
|
-
' Not port 6543 (the transaction pooler), and not the direct',
|
|
47
|
-
' db.<project>.supabase.co address - both are warned about.',
|
|
48
|
-
'',
|
|
49
|
-
' Lovable your app uses Supabase underneath. Open the Supabase',
|
|
50
|
-
' and Bolt project it made for you and follow the lines above.',
|
|
51
|
-
'',
|
|
52
|
-
' Neon console.neon.tech -> your project -> Connection Details.',
|
|
53
|
-
'',
|
|
54
|
-
'It is one long line beginning postgresql:// and it ends in /postgres.',
|
|
55
|
-
];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* What is about to happen, and what is not.
|
|
60
|
-
*
|
|
61
|
-
* The second half matters more than the first. Everybody who runs this is
|
|
62
|
-
* being asked to hand a stranger's program the key to their live database,
|
|
63
|
-
* and the fears they have are specific: will you read my customers, will you
|
|
64
|
-
* send anything anywhere, will you leave a mess. Each of those is answered in
|
|
65
|
-
* its own line, in the words they would use.
|
|
66
|
-
*
|
|
67
|
-
* Nothing here is a promise made only on this screen. Every line is something
|
|
68
|
-
* the code is built to make true and a check in this repo fails if it stops
|
|
69
|
-
* being true - `untouched.check.js` photographs the whole database and
|
|
70
|
-
* compares it afterwards.
|
|
71
|
-
*/
|
|
72
|
-
function consentLines(target) {
|
|
73
|
-
return [
|
|
74
|
-
'Before I touch anything, here is exactly what I am going to do.',
|
|
75
|
-
'',
|
|
76
|
-
'What I will do:',
|
|
77
|
-
'',
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
'',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
' -
|
|
85
|
-
'
|
|
86
|
-
'',
|
|
87
|
-
|
|
88
|
-
'',
|
|
89
|
-
' -
|
|
90
|
-
'
|
|
91
|
-
'',
|
|
92
|
-
' -
|
|
93
|
-
'',
|
|
94
|
-
'
|
|
95
|
-
'',
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'',
|
|
100
|
-
' -
|
|
101
|
-
'',
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'',
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'',
|
|
109
|
-
'
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
'
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'
|
|
151
|
-
'',
|
|
152
|
-
'
|
|
153
|
-
'',
|
|
154
|
-
'
|
|
155
|
-
'',
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'',
|
|
160
|
-
|
|
161
|
-
'',
|
|
162
|
-
'
|
|
163
|
-
'',
|
|
164
|
-
' -
|
|
165
|
-
'
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
|
|
169
|
-
'',
|
|
170
|
-
'
|
|
171
|
-
'',
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
rl.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
say('');
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
whereToFindIt
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1
|
+
// What the person is asked, and what they are told before anything runs.
|
|
2
|
+
//
|
|
3
|
+
// This file is the whole difference between a tool for people who write code
|
|
4
|
+
// and a tool for people who do not. The engine behind it does not change; the
|
|
5
|
+
// first ninety seconds do.
|
|
6
|
+
//
|
|
7
|
+
// Two things happen here and both are deliberate.
|
|
8
|
+
//
|
|
9
|
+
// Asking, rather than requiring. `KN_DATABASE_URL=... node scan.js public` is
|
|
10
|
+
// three unfamiliar ideas at once: an environment variable, a connection
|
|
11
|
+
// string, and a schema. Somebody who has only ever clicked Deploy has met
|
|
12
|
+
// none of them. So the command takes no arguments, and asks.
|
|
13
|
+
//
|
|
14
|
+
// Telling before doing. This tool connects to a person's production database
|
|
15
|
+
// with the owner's credential and creates things in it. Nobody should agree
|
|
16
|
+
// to that from a package name. What it does, what it does not do, and then a
|
|
17
|
+
// yes or no - in that order, before the first statement is sent.
|
|
18
|
+
|
|
19
|
+
const readline = require('readline');
|
|
20
|
+
const trouble = require('./trouble.js');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where the connection string is, said as buttons rather than as concepts.
|
|
24
|
+
*
|
|
25
|
+
* Named screen by screen because "get your connection string" is not an
|
|
26
|
+
* instruction to somebody who does not know the phrase. Supabase first
|
|
27
|
+
* because that is who this is for; Lovable and Bolt build on Supabase, so
|
|
28
|
+
* their users end up on the same page by a different door.
|
|
29
|
+
*/
|
|
30
|
+
function whereToFindIt() {
|
|
31
|
+
// The session pooler, and said as such: the direct connection answers only
|
|
32
|
+
// over IPv6, which many home networks do not have, and the transaction
|
|
33
|
+
// pooler on 6543 cannot hold these attacks together. The kryptheon
|
|
34
|
+
// dashboard shows these same steps; kryptheon-night words hands them over.
|
|
35
|
+
return [
|
|
36
|
+
'Where to find it:',
|
|
37
|
+
'',
|
|
38
|
+
' Supabase 1. supabase.com/dashboard -> open your project.',
|
|
39
|
+
' 2. Press "Connect" at the top of the page.',
|
|
40
|
+
' 3. Choose "Session pooler" - the one on port 5432.',
|
|
41
|
+
' 4. Copy the URI. It starts postgresql://',
|
|
42
|
+
' 5. Replace [YOUR-PASSWORD] in it with your database password.',
|
|
43
|
+
' Forgotten it? Project Settings -> Database -> Reset',
|
|
44
|
+
' database password.',
|
|
45
|
+
'',
|
|
46
|
+
' Not port 6543 (the transaction pooler), and not the direct',
|
|
47
|
+
' db.<project>.supabase.co address - both are warned about.',
|
|
48
|
+
'',
|
|
49
|
+
' Lovable your app uses Supabase underneath. Open the Supabase',
|
|
50
|
+
' and Bolt project it made for you and follow the lines above.',
|
|
51
|
+
'',
|
|
52
|
+
' Neon console.neon.tech -> your project -> Connection Details.',
|
|
53
|
+
'',
|
|
54
|
+
'It is one long line beginning postgresql:// and it ends in /postgres.',
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What is about to happen, and what is not.
|
|
60
|
+
*
|
|
61
|
+
* The second half matters more than the first. Everybody who runs this is
|
|
62
|
+
* being asked to hand a stranger's program the key to their live database,
|
|
63
|
+
* and the fears they have are specific: will you read my customers, will you
|
|
64
|
+
* send anything anywhere, will you leave a mess. Each of those is answered in
|
|
65
|
+
* its own line, in the words they would use.
|
|
66
|
+
*
|
|
67
|
+
* Nothing here is a promise made only on this screen. Every line is something
|
|
68
|
+
* the code is built to make true and a check in this repo fails if it stops
|
|
69
|
+
* being true - `untouched.check.js` photographs the whole database and
|
|
70
|
+
* compares it afterwards.
|
|
71
|
+
*/
|
|
72
|
+
function consentLines(target) {
|
|
73
|
+
return [
|
|
74
|
+
'Before I touch anything, here is exactly what I am going to do.',
|
|
75
|
+
'',
|
|
76
|
+
'What I will do:',
|
|
77
|
+
'',
|
|
78
|
+
].concat(target ? [
|
|
79
|
+
' - Look at the shape of your "' + target + '" tables: their names, their',
|
|
80
|
+
' columns, and the rules about who is allowed to see what.',
|
|
81
|
+
] : [
|
|
82
|
+
// No place named: "public", or the one other place all the app's tables
|
|
83
|
+
// are in (blind round 17) - said here, before anything is connected to.
|
|
84
|
+
' - Look at the shape of your app\'s tables - in "public", or wherever',
|
|
85
|
+
' they all are if that is somewhere else: their names, their columns,',
|
|
86
|
+
' and the rules about who is allowed to see what.',
|
|
87
|
+
], [
|
|
88
|
+
'',
|
|
89
|
+
' - Look at your file storage settings: which buckets are public, and',
|
|
90
|
+
' the rules about who may open their files. Never a file itself.',
|
|
91
|
+
'',
|
|
92
|
+
' - Make one new temporary space inside your database and rebuild that',
|
|
93
|
+
' same shape in it. Your real tables are not changed.',
|
|
94
|
+
'',
|
|
95
|
+
' - Put two made-up people in the copy - fake names, fake emails.',
|
|
96
|
+
'',
|
|
97
|
+
' - Attack the copy. I try to read those two fake people as a stranger',
|
|
98
|
+
' would, try to change their rows, try to break them.',
|
|
99
|
+
'',
|
|
100
|
+
' - Tell you what got through, and delete the copy.',
|
|
101
|
+
'',
|
|
102
|
+
'What I will not do:',
|
|
103
|
+
'',
|
|
104
|
+
' - I do not read your real data. Not one customer, order or message.',
|
|
105
|
+
' Apart from the storage settings above, every row I read is a row I',
|
|
106
|
+
' put there myself a moment earlier.',
|
|
107
|
+
'',
|
|
108
|
+
' - I do not change your real tables. Only the temporary copy.',
|
|
109
|
+
'',
|
|
110
|
+
' - I do not send anything anywhere. No account, no upload, no server of',
|
|
111
|
+
' mine. Your connection string stays on this computer and is not',
|
|
112
|
+
' written to any file.',
|
|
113
|
+
'',
|
|
114
|
+
' - I do not leave anything behind. The copy is deleted when I finish,',
|
|
115
|
+
' and also if I crash.',
|
|
116
|
+
'',
|
|
117
|
+
'This takes two or three minutes.',
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What staying costs, said before anything is left behind.
|
|
123
|
+
*
|
|
124
|
+
* The scan's consent screen above ends "I do not leave anything behind", and
|
|
125
|
+
* that is true of a scan and false of an install. Reusing it here would be
|
|
126
|
+
* the product lying on the one screen it exists to be honest on, so this is
|
|
127
|
+
* its own text and says the opposite plainly, first.
|
|
128
|
+
*
|
|
129
|
+
* Everything that gets left is named, because "it installs a few things" is
|
|
130
|
+
* the sentence somebody agrees to and then finds a `cron` schema they did not
|
|
131
|
+
* recognise. And the way out is on the screen where they say yes, not in a
|
|
132
|
+
* README they will not read again.
|
|
133
|
+
*/
|
|
134
|
+
function installConsentLines(schema, source, at, reporting) {
|
|
135
|
+
// Said as it is: with a project token the night does send something, so
|
|
136
|
+
// "sends nothing anywhere" would be a lie on exactly the screen that asks.
|
|
137
|
+
const sends = reporting
|
|
138
|
+
? [
|
|
139
|
+
' - It still never reads your real data. Once a night it sends your',
|
|
140
|
+
' Kryptheon dashboard a summary of what it found: the kind of each',
|
|
141
|
+
' finding and the table it is on. Never a row, never a value, never',
|
|
142
|
+
' your connection string. That is what your morning report and',
|
|
143
|
+
' alerts are made from.',
|
|
144
|
+
]
|
|
145
|
+
: [
|
|
146
|
+
' - It still never reads your real data, and still sends nothing anywhere.',
|
|
147
|
+
' Nothing leaves your database, because the checking happens inside it.',
|
|
148
|
+
];
|
|
149
|
+
return [
|
|
150
|
+
'This one stays. Everything else this tool does is temporary; this is not.',
|
|
151
|
+
'',
|
|
152
|
+
'What I will leave in your database:',
|
|
153
|
+
'',
|
|
154
|
+
' - A schema called "' + schema + '", holding the functions that do the',
|
|
155
|
+
' checking. No data of yours goes in it.',
|
|
156
|
+
'',
|
|
157
|
+
' - Two Postgres extensions, if they are not already there: pg_cron, which',
|
|
158
|
+
' runs things on a schedule, and pg_net. Supabase provides both.',
|
|
159
|
+
'',
|
|
160
|
+
' - One scheduled job, named kryptheon_nightly, set to ' + at + '.',
|
|
161
|
+
'',
|
|
162
|
+
'What it will do, every night:',
|
|
163
|
+
'',
|
|
164
|
+
' - Exactly what the scan does now - copy the shape of your "' + source + '"',
|
|
165
|
+
' tables into a temporary space, put two made-up people in it, attack',
|
|
166
|
+
' that, write down what got through, and delete the copy.',
|
|
167
|
+
'',
|
|
168
|
+
...sends,
|
|
169
|
+
'',
|
|
170
|
+
'One thing it cannot do:',
|
|
171
|
+
'',
|
|
172
|
+
' - It cannot race two requests against each other, which is how the',
|
|
173
|
+
' "can this exist twice" question is answered. That needs two',
|
|
174
|
+
' connections at the same instant, and nothing living inside a database',
|
|
175
|
+
' has them. The nightly report names every column it could not race.',
|
|
176
|
+
' Running the command by hand still tests them properly.',
|
|
177
|
+
'',
|
|
178
|
+
'To take it all away again: npx kryptheon-night uninstall',
|
|
179
|
+
'',
|
|
180
|
+
'That removes the job, the schema, and any extension I had to add - and',
|
|
181
|
+
'leaves alone anything that was already here.',
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Asks a question and hands back what was typed - or null when there is no
|
|
187
|
+
* more input to read, so a closed keyboard is an answer and not a wait forever.
|
|
188
|
+
*/
|
|
189
|
+
function ask(question, io) {
|
|
190
|
+
const streams = io || { input: process.stdin, output: process.stdout };
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
const rl = readline.createInterface({ input: streams.input, output: streams.output });
|
|
193
|
+
let answered = false;
|
|
194
|
+
rl.on('close', () => {
|
|
195
|
+
if (!answered) resolve(null);
|
|
196
|
+
});
|
|
197
|
+
rl.question(question, (answer) => {
|
|
198
|
+
answered = true;
|
|
199
|
+
rl.close();
|
|
200
|
+
resolve(String(answer || '').trim());
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Asks for something that must not be echoed.
|
|
207
|
+
*
|
|
208
|
+
* The connection string carries the password to the entire database. Shown on
|
|
209
|
+
* screen it is read by whoever is behind them, and it stays in the scrollback
|
|
210
|
+
* of whatever terminal they are in for as long as that window is open.
|
|
211
|
+
*
|
|
212
|
+
* Nothing is printed as they type - not even stars, because a pasted string
|
|
213
|
+
* of stars is no more checkable than nothing and the character count alone
|
|
214
|
+
* says how long the password is. What they get instead is the address read
|
|
215
|
+
* back to them afterwards, with the password removed, which is the part they
|
|
216
|
+
* would actually want to check.
|
|
217
|
+
*/
|
|
218
|
+
function askSecret(question) {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
221
|
+
let silent = false;
|
|
222
|
+
// readline writes every keystroke back to the screen itself. This replaces
|
|
223
|
+
// that with nothing, but only once the prompt has been written - otherwise
|
|
224
|
+
// the question never appears either.
|
|
225
|
+
rl._writeToOutput = function (text) {
|
|
226
|
+
if (!silent) rl.output.write(text);
|
|
227
|
+
};
|
|
228
|
+
rl.question(question, (answer) => {
|
|
229
|
+
silent = false;
|
|
230
|
+
rl.output.write('\n');
|
|
231
|
+
rl.close();
|
|
232
|
+
resolve(String(answer || '').trim());
|
|
233
|
+
});
|
|
234
|
+
silent = true;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Was that a yes?
|
|
240
|
+
*
|
|
241
|
+
* The question being answered is "may I connect to your production database",
|
|
242
|
+
* so the rule is that only a yes is a yes. A stray newline, a shrug, an
|
|
243
|
+
* accidental Enter on an empty line - none of those are permission, and the
|
|
244
|
+
* cost of getting this backwards is connecting to somebody's live database
|
|
245
|
+
* without being asked to.
|
|
246
|
+
*
|
|
247
|
+
* Separate from the asking so it can be checked directly. Every form of this
|
|
248
|
+
* that hid inside a prompt went unchecked.
|
|
249
|
+
*/
|
|
250
|
+
function isYes(answer) {
|
|
251
|
+
const said = String(answer == null ? '' : answer).trim().toLowerCase();
|
|
252
|
+
return said === 'y' || said === 'yes';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Yes or no, where anything that is not a clear yes is a no - except an empty
|
|
257
|
+
* line, which is asked again.
|
|
258
|
+
*
|
|
259
|
+
* On Windows a pasted connection string can carry its own line ending: the
|
|
260
|
+
* hidden prompt takes it and moves on, and the Enter the person then presses
|
|
261
|
+
* to "send" the paste lands here, on a question they have not read yet. Taken
|
|
262
|
+
* as a no, it stopped an install nobody had said no to. An empty line is
|
|
263
|
+
* still never a yes; it is only not an answer.
|
|
264
|
+
*/
|
|
265
|
+
async function askYesNo(question, io) {
|
|
266
|
+
const streams = io || { input: process.stdin, output: process.stdout };
|
|
267
|
+
for (;;) {
|
|
268
|
+
const answer = await ask(question, streams);
|
|
269
|
+
if (answer === null) return false; // nothing more can be typed
|
|
270
|
+
if (answer !== '') return isYes(answer);
|
|
271
|
+
streams.output.write(' Type y and press Enter to go ahead, or n to stop.\n');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The connection string, from wherever it can be had.
|
|
277
|
+
*
|
|
278
|
+
* The environment variable is checked first so that anybody scripting this
|
|
279
|
+
* never sees a prompt, and so the string does not have to be retyped on every
|
|
280
|
+
* run. Nothing is saved: asked again next time is the correct behaviour for a
|
|
281
|
+
* credential this powerful, and a file holding it would be the one thing this
|
|
282
|
+
* tool tells people not to do.
|
|
283
|
+
*/
|
|
284
|
+
async function askForConnection(say) {
|
|
285
|
+
say('');
|
|
286
|
+
say(' I need the connection string for your database.');
|
|
287
|
+
say('');
|
|
288
|
+
say(' It is one line that lets me connect. It contains your database');
|
|
289
|
+
say(' password, so I will not show it on screen as you paste it, and I do');
|
|
290
|
+
say(' not save it anywhere.');
|
|
291
|
+
say('');
|
|
292
|
+
whereToFindIt().forEach((l) => say(' ' + l));
|
|
293
|
+
say('');
|
|
294
|
+
|
|
295
|
+
const given = await askSecret(' Paste it here and press Enter: ');
|
|
296
|
+
return given;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
whereToFindIt: whereToFindIt,
|
|
301
|
+
consentLines: consentLines,
|
|
302
|
+
installConsentLines: installConsentLines,
|
|
303
|
+
ask: ask,
|
|
304
|
+
askSecret: askSecret,
|
|
305
|
+
askYesNo: askYesNo,
|
|
306
|
+
isYes: isYes,
|
|
307
|
+
askForConnection: askForConnection,
|
|
308
|
+
// Re-exported so the command has one place to reach for the wording of a
|
|
309
|
+
// failure, rather than two.
|
|
310
|
+
withoutSecret: trouble.withoutSecret,
|
|
311
|
+
};
|