kryptheon-night 0.1.14 → 0.1.19
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/bin/kryptheon-night.js +34 -8
- package/finding.js +43 -8
- package/installer.js +16 -0
- package/package.json +3 -2
- package/scan.js +84 -7
- package/store.js +2 -0
- package/trouble.js +11 -6
package/bin/kryptheon-night.js
CHANGED
|
@@ -118,6 +118,30 @@ function readArgs(argv) {
|
|
|
118
118
|
return asked;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Keeps what the nightly run last said where the kryptheon dashboard reads
|
|
123
|
+
* it, so the answer waiting in the database is also on the page - not only
|
|
124
|
+
* what was last run from this terminal. Never fails anything: the person has
|
|
125
|
+
* their answer already, and a folder that cannot be written is not theirs to
|
|
126
|
+
* debug here.
|
|
127
|
+
*/
|
|
128
|
+
function keepTheNight(latest) {
|
|
129
|
+
try {
|
|
130
|
+
scanner.saveNightly(scanner.nightlyFile(), scanner.nightlyRecord(latest, new Date().toISOString()));
|
|
131
|
+
} catch (err) {
|
|
132
|
+
/* the dashboard keeps its older copy, which carries its own date */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Reads and keeps it, for the doors that do not already hold the answer. */
|
|
137
|
+
async function readAndKeepTheNight(client) {
|
|
138
|
+
try {
|
|
139
|
+
keepTheNight(await installer.latestRun(client, {}));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
/* as above */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
121
145
|
/**
|
|
122
146
|
* The three doors that are not a scan.
|
|
123
147
|
*
|
|
@@ -127,6 +151,7 @@ function readArgs(argv) {
|
|
|
127
151
|
*/
|
|
128
152
|
async function runVerb(command, client, target) {
|
|
129
153
|
if (command === 'status') {
|
|
154
|
+
await readAndKeepTheNight(client);
|
|
130
155
|
const where = await installer.status(client, {});
|
|
131
156
|
if (!where) {
|
|
132
157
|
line(' The nightly run is not installed in this database.');
|
|
@@ -183,6 +208,7 @@ async function runVerb(command, client, target) {
|
|
|
183
208
|
return 0;
|
|
184
209
|
}
|
|
185
210
|
const removed = await installer.uninstall(client, {});
|
|
211
|
+
await readAndKeepTheNight(client);
|
|
186
212
|
line(' Removed.');
|
|
187
213
|
line('');
|
|
188
214
|
line(' the nightly job ' + (removed.job ? 'gone' : 'there was none'));
|
|
@@ -195,7 +221,9 @@ async function runVerb(command, client, target) {
|
|
|
195
221
|
}
|
|
196
222
|
|
|
197
223
|
if (command === 'night') {
|
|
198
|
-
const
|
|
224
|
+
const latest = await installer.latestRun(client, {});
|
|
225
|
+
keepTheNight(latest);
|
|
226
|
+
const where = latest.where;
|
|
199
227
|
if (!where) {
|
|
200
228
|
line(' The nightly run is not installed in this database, so there is');
|
|
201
229
|
line(' nothing to read back.');
|
|
@@ -205,12 +233,7 @@ async function runVerb(command, client, target) {
|
|
|
205
233
|
line('');
|
|
206
234
|
return 2;
|
|
207
235
|
}
|
|
208
|
-
|
|
209
|
-
const { rows } = await client.query(
|
|
210
|
-
'SELECT ran_at, source, stopped, attacks_run, findings, not_checked ' +
|
|
211
|
-
'FROM ' + quoted + '.runs ORDER BY ran_at DESC LIMIT 1',
|
|
212
|
-
);
|
|
213
|
-
if (!rows.length) {
|
|
236
|
+
if (!latest.run) {
|
|
214
237
|
// Installed and never run is not the same as run and found nothing, and
|
|
215
238
|
// saying "nothing got through" here would be the worst sentence in the
|
|
216
239
|
// product printed about a night that never happened.
|
|
@@ -220,7 +243,7 @@ async function runVerb(command, client, target) {
|
|
|
220
243
|
line('');
|
|
221
244
|
return 0;
|
|
222
245
|
}
|
|
223
|
-
const run =
|
|
246
|
+
const run = latest.run;
|
|
224
247
|
line(' From the night of ' + new Date(run.ran_at).toISOString().slice(0, 16).replace('T', ' ') +
|
|
225
248
|
', on "' + run.source + '":');
|
|
226
249
|
scanner.report({
|
|
@@ -583,6 +606,9 @@ async function main() {
|
|
|
583
606
|
// think about it again, which is exactly the app that is unprotected
|
|
584
607
|
// three months later. The tool already knows whether the nightly run is
|
|
585
608
|
// there. So it asks.
|
|
609
|
+
// Whatever the nightly run last said is kept for the dashboard too, so
|
|
610
|
+
// the page shows both answers and says which is which.
|
|
611
|
+
await readAndKeepTheNight(client);
|
|
586
612
|
if (!result.stopped) await offerTheNight(client, target, canAsk);
|
|
587
613
|
} catch (err) {
|
|
588
614
|
// Anything that went wrong mid-scan. The copy has already been dropped by
|
package/finding.js
CHANGED
|
@@ -523,8 +523,32 @@ function serverOnlyLines(finding) {
|
|
|
523
523
|
// column saying whose row it is. The assistant applying it had to correct
|
|
524
524
|
// the prompt; a less careful one would have locked the table away from the
|
|
525
525
|
// members who use it.
|
|
526
|
-
const
|
|
527
|
-
|
|
526
|
+
const team = finding.authTied !== false && (finding.columns || []).find((c) => TENANT_COLUMN.test(String(c)));
|
|
527
|
+
const maker = finding.owner || finding.ownedBy;
|
|
528
|
+
// A team's row that also says who made it - created_by beside workspace_id.
|
|
529
|
+
// Found on a benchmark: the prompt said to compare created_by to auth.uid()
|
|
530
|
+
// for reading, which hides every teammate's rows and breaks the shared
|
|
531
|
+
// view the table exists for. Who made a row decides who may write it
|
|
532
|
+
// under their name, not who may see it.
|
|
533
|
+
if (team && finding.owned !== false && maker) {
|
|
534
|
+
return [
|
|
535
|
+
'Rows in "' + table + '" belong to a team ("' + team + '"), and "' + maker + '" only records who ' +
|
|
536
|
+
'made each one. So reading should be decided by team membership: let a signed-in user read a ' +
|
|
537
|
+
'row when they are a member of that row\'s "' + team + '" - checked through your membership ' +
|
|
538
|
+
'table, or a helper function that asks it - and give logged-out visitors nothing. Do not ' +
|
|
539
|
+
'compare "' + maker + '" to auth.uid() for reading: that hides teammates\' rows from each other ' +
|
|
540
|
+
'and breaks the feature.',
|
|
541
|
+
'',
|
|
542
|
+
'Write separate rules for reading, inserting, updating and deleting. Inserting: WITH CHECK that ' +
|
|
543
|
+
'the user is a member of the row\'s "' + team + '" AND "' + maker + '" = auth.uid(), so nobody ' +
|
|
544
|
+
'writes a row under somebody else\'s name or into a team they are not in. Updating and ' +
|
|
545
|
+
'deleting: the membership check in USING (and WITH CHECK for updating), and decide whether any ' +
|
|
546
|
+
'member may do it or only the row\'s "' + maker + '" or an admin. If only your server writes ' +
|
|
547
|
+
'this table, add no write rules at all.',
|
|
548
|
+
'',
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
const tenant = finding.owned === false && team;
|
|
528
552
|
if (tenant) {
|
|
529
553
|
return [
|
|
530
554
|
'Rows in "' + table + '" belong to an organization ("' + tenant + '"), not to one person. So the ' +
|
|
@@ -563,6 +587,18 @@ function serverOnlyLines(finding) {
|
|
|
563
587
|
];
|
|
564
588
|
}
|
|
565
589
|
|
|
590
|
+
/**
|
|
591
|
+
* The opening of a rules fix. "Switch row level security on" is said only
|
|
592
|
+
* when it is off: told it about a table where it was already on, the person
|
|
593
|
+
* looks for a switch that is already flipped and misses the rule that is the
|
|
594
|
+
* real problem.
|
|
595
|
+
*/
|
|
596
|
+
function switchOn(finding) {
|
|
597
|
+
return finding.rlsEnabled
|
|
598
|
+
? 'Row level security is already on for this table, so the rules on it are the problem: '
|
|
599
|
+
: 'Switch row level security on for this table, then ';
|
|
600
|
+
}
|
|
601
|
+
|
|
566
602
|
/**
|
|
567
603
|
* One prompt for everything wrong with one table's rules - read and write,
|
|
568
604
|
* strangers and customers - because it is one rule to replace, not four.
|
|
@@ -597,9 +633,8 @@ function groupPromptFor(members) {
|
|
|
597
633
|
'',
|
|
598
634
|
].concat(
|
|
599
635
|
serverOnlyLines(lead) || [
|
|
600
|
-
|
|
601
|
-
'
|
|
602
|
-
'covers far more than reading.',
|
|
636
|
+
switchOn(lead) + 'remove the rule that lets everyone in, and write separate rules for reading, ' +
|
|
637
|
+
'inserting, updating and deleting. A rule written FOR ALL covers far more than reading.',
|
|
603
638
|
'',
|
|
604
639
|
'Each of them should compare ' + ownerWords + ' to the id of the signed-in user (auth.uid()): ' +
|
|
605
640
|
'USING for reading, updating and deleting, WITH CHECK for inserting and updating, so nobody ' +
|
|
@@ -706,9 +741,9 @@ function fixPromptFor(finding) {
|
|
|
706
741
|
'',
|
|
707
742
|
].concat(
|
|
708
743
|
serverOnlyLines(finding) || [
|
|
709
|
-
|
|
710
|
-
'
|
|
711
|
-
'
|
|
744
|
+
switchOn(finding) + 'write separate rules for reading, inserting, updating and deleting. ' +
|
|
745
|
+
'A rule written FOR SELECT does not cover writes, and a rule written FOR ALL covers ' +
|
|
746
|
+
'far more than reading.',
|
|
712
747
|
'',
|
|
713
748
|
'For insert and update, use WITH CHECK comparing ' + owner +
|
|
714
749
|
' to the id of the signed-in user, so nobody can write a row under somebody ' +
|
package/installer.js
CHANGED
|
@@ -229,8 +229,24 @@ async function uninstall(client, options) {
|
|
|
229
229
|
return removed;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* What the nightly run last said, read out of the database: whether it is
|
|
234
|
+
* installed at all, how it is scheduled, and its newest row in runs - or null
|
|
235
|
+
* for a run that has not happened yet. Read only.
|
|
236
|
+
*/
|
|
237
|
+
async function latestRun(client, options) {
|
|
238
|
+
const where = await status(client, options);
|
|
239
|
+
if (!where) return { installed: false, where: null, run: null };
|
|
240
|
+
const { rows } = await client.query(
|
|
241
|
+
'SELECT ran_at, source, stopped, attacks_run, findings, not_checked FROM ' +
|
|
242
|
+
quote(where.schema) + '.runs ORDER BY ran_at DESC, id DESC LIMIT 1',
|
|
243
|
+
);
|
|
244
|
+
return { installed: true, where: where, run: rows[0] || null };
|
|
245
|
+
}
|
|
246
|
+
|
|
232
247
|
module.exports = {
|
|
233
248
|
SCHEMA: SCHEMA,
|
|
249
|
+
latestRun: latestRun,
|
|
234
250
|
AT: AT,
|
|
235
251
|
install: install,
|
|
236
252
|
uninstall: uninstall,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon-night",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Attacks a copy of your Supabase database and tells you in plain English what got in.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=18.0.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"check": "node package.check.js && node store.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node outside.check.js && node recheck.check.js && node untouched.check.js && node blocked.check.js && node orphans.check.js && node guests.check.js && node usable.check.js && node twin.check.js && node engines.check.js && node external.check.js && node schema.check.js && node collision.check.js && node tamper.check.js && node orphan.check.js && node shapes.check.js && node verdicts.check.js && node shop.check.js && node privileged.check.js && node recursion.check.js && node guarded.check.js && node cli.check.js && node loop.check.js",
|
|
34
|
+
"check": "node package.check.js && node store.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node outside.check.js && node recheck.check.js && node untouched.check.js && node blocked.check.js && node orphans.check.js && node guests.check.js && node usable.check.js && node twin.check.js && node engines.check.js && node external.check.js && node schema.check.js && node collision.check.js && node tamper.check.js && node orphan.check.js && node shapes.check.js && node verdicts.check.js && node shop.check.js && node privileged.check.js && node recursion.check.js && node guarded.check.js && node nightly.check.js && node cli.check.js && node loop.check.js",
|
|
35
35
|
"check:dry": "node package.check.js && node store.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node recheck.check.js && node outside.check.js",
|
|
36
36
|
"check:package": "node package.check.js",
|
|
37
37
|
"check:store": "node store.check.js",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"check:privileged": "node privileged.check.js",
|
|
56
56
|
"check:recursion": "node recursion.check.js",
|
|
57
57
|
"check:guarded": "node guarded.check.js",
|
|
58
|
+
"check:nightly": "node nightly.check.js",
|
|
58
59
|
"check:cli": "node cli.check.js",
|
|
59
60
|
"check:writes": "node tamper.check.js",
|
|
60
61
|
"check:orphans": "node orphans.check.js",
|
package/scan.js
CHANGED
|
@@ -403,10 +403,14 @@ async function scan(client, sourceSchema, options) {
|
|
|
403
403
|
const ties = finding.authTiesOf(plan);
|
|
404
404
|
// And whether a row says whose it is: with no such column, no rule can
|
|
405
405
|
// tie it to the person asking. Read from the copy, the way seeding did.
|
|
406
|
-
|
|
406
|
+
// The column's name travels too: a team's table that also says who made
|
|
407
|
+
// each row needs it for its prompt, even on a finding that is not about
|
|
408
|
+
// owners at all.
|
|
409
|
+
const owned = new Map(theirs.map((table) => [table.name, attack.ownerColumn(table)]));
|
|
407
410
|
const tagged = (f) => Object.assign({}, f,
|
|
408
411
|
ties.has(f.table) ? { authTied: ties.get(f.table) } : {},
|
|
409
|
-
owned.has(f.table) ? { owned: owned.get(f.table) } : {}
|
|
412
|
+
owned.has(f.table) ? { owned: Boolean(owned.get(f.table)) } : {},
|
|
413
|
+
owned.get(f.table) ? { ownedBy: owned.get(f.table) } : {});
|
|
410
414
|
|
|
411
415
|
return {
|
|
412
416
|
stopped: null,
|
|
@@ -438,6 +442,51 @@ function exitCodeFor(result) {
|
|
|
438
442
|
return (result.findings || []).some((f) => f.status !== 'verification required') ? 1 : 0;
|
|
439
443
|
}
|
|
440
444
|
|
|
445
|
+
/** Where the nightly run's last answer is kept for the dashboard. */
|
|
446
|
+
function nightlyFile() {
|
|
447
|
+
return store.open(process.cwd()).nightNightly;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The nightly run's last answer, in the shape the dashboard reads: the same
|
|
452
|
+
* described findings a scan saves, and the three states that are not the same
|
|
453
|
+
* as "nothing found" - not installed, installed and never run, and stopped -
|
|
454
|
+
* said as themselves. `latest` is installer.latestRun's answer.
|
|
455
|
+
*/
|
|
456
|
+
function nightlyRecord(latest, readAt) {
|
|
457
|
+
const at = (value) => (value ? new Date(value).toISOString() : null);
|
|
458
|
+
if (!latest || !latest.installed) return { readAt: readAt, installed: false };
|
|
459
|
+
const where = latest.where || {};
|
|
460
|
+
const record = {
|
|
461
|
+
readAt: readAt,
|
|
462
|
+
installed: true,
|
|
463
|
+
scheduled: where.job ? where.job.schedule : null,
|
|
464
|
+
active: Boolean(where.job && where.job.active),
|
|
465
|
+
source: where.source || null,
|
|
466
|
+
ranAt: null,
|
|
467
|
+
};
|
|
468
|
+
const run = latest.run;
|
|
469
|
+
if (!run) return record;
|
|
470
|
+
return Object.assign(record, {
|
|
471
|
+
ranAt: at(run.ran_at),
|
|
472
|
+
source: run.source,
|
|
473
|
+
stopped: run.stopped || null,
|
|
474
|
+
attacksRun: run.attacks_run || 0,
|
|
475
|
+
notChecked: run.not_checked || [],
|
|
476
|
+
findings: run.stopped ? [] : finding.describeAll(run.findings || []),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Kept for the dashboard. A failure here never touches the run itself. */
|
|
481
|
+
function saveNightly(file, record) {
|
|
482
|
+
try {
|
|
483
|
+
fs.writeFileSync(file, JSON.stringify(record, null, 2));
|
|
484
|
+
return true;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
441
490
|
/** The last run, or null if there has not been one worth keeping. */
|
|
442
491
|
function loadLastRun(file) {
|
|
443
492
|
try {
|
|
@@ -506,6 +555,34 @@ function notTestedLines(result) {
|
|
|
506
555
|
return lines;
|
|
507
556
|
}
|
|
508
557
|
|
|
558
|
+
/**
|
|
559
|
+
* The warning above the verdict: how much went untested, counted honestly.
|
|
560
|
+
*
|
|
561
|
+
* Every entry used to be counted as a table. Found on a benchmark: one
|
|
562
|
+
* column's duplicate test did not run, and the top line said "1 table was
|
|
563
|
+
* NOT checked" about a table every other attack had covered - and a table
|
|
564
|
+
* with two attacks stuck was counted as two tables. An entry with no attack
|
|
565
|
+
* named is a whole table nothing could be put into; one with an attack named
|
|
566
|
+
* is part of a table.
|
|
567
|
+
*/
|
|
568
|
+
function missedLines(notChecked) {
|
|
569
|
+
const tableOf = (entry) => String(entry.table).split('.')[0];
|
|
570
|
+
const whole = new Set(notChecked.filter((entry) => !entry.key).map(tableOf));
|
|
571
|
+
const parts = notChecked.filter((entry) => entry.key && !whole.has(tableOf(entry)));
|
|
572
|
+
const partly = new Set(parts.map(tableOf));
|
|
573
|
+
const tables = (n) => n + (n === 1 ? ' table' : ' tables');
|
|
574
|
+
const said = [];
|
|
575
|
+
if (whole.size) said.push(tables(whole.size) + (whole.size === 1 ? ' was' : ' were') + ' NOT checked');
|
|
576
|
+
if (parts.length) {
|
|
577
|
+
said.push(parts.length + (parts.length === 1 ? ' check' : ' checks') + ' on ' +
|
|
578
|
+
(whole.size ? tables(partly.size).replace(/ table/, ' other table') : tables(partly.size)) +
|
|
579
|
+
(parts.length === 1 ? ' was' : ' were') + ' NOT run');
|
|
580
|
+
}
|
|
581
|
+
const it = notChecked.length === 1 ? 'it' : 'them';
|
|
582
|
+
return wrap(said.join(', and ') + '. Whatever this report says next is not about ' + it + '. Treat ' +
|
|
583
|
+
it + ' as unknown, not as clear - the list, and why, is at the end.', 68).map((l) => ' ' + l);
|
|
584
|
+
}
|
|
585
|
+
|
|
509
586
|
/** The report, as the person reads it in the morning. */
|
|
510
587
|
function report(result) {
|
|
511
588
|
if (result.stopped) {
|
|
@@ -530,11 +607,7 @@ function report(result) {
|
|
|
530
607
|
const sayWhatWasMissed = () => {
|
|
531
608
|
if (!notChecked.length) return;
|
|
532
609
|
line('');
|
|
533
|
-
|
|
534
|
-
' NOT checked. Whatever this report says next is not');
|
|
535
|
-
line(' about ' + (notChecked.length === 1 ? 'it' : 'them') + '. Treat ' +
|
|
536
|
-
(notChecked.length === 1 ? 'it' : 'them') + ' as unknown, not as clear - the list, and why, is');
|
|
537
|
-
line(' at the end.');
|
|
610
|
+
missedLines(notChecked).forEach(line);
|
|
538
611
|
};
|
|
539
612
|
|
|
540
613
|
if (!result.findings.length) {
|
|
@@ -702,9 +775,13 @@ if (require.main === module) {
|
|
|
702
775
|
module.exports = {
|
|
703
776
|
scan: scan,
|
|
704
777
|
report: report,
|
|
778
|
+
missedLines: missedLines,
|
|
705
779
|
wrap: wrap,
|
|
706
780
|
loadLastRun: loadLastRun,
|
|
707
781
|
saveRun: saveRun,
|
|
782
|
+
nightlyFile: nightlyFile,
|
|
783
|
+
nightlyRecord: nightlyRecord,
|
|
784
|
+
saveNightly: saveNightly,
|
|
708
785
|
sweepOldCopies: sweepOldCopies,
|
|
709
786
|
notTestedLines: notTestedLines,
|
|
710
787
|
lastRunFile: lastRunFile,
|
package/store.js
CHANGED
|
@@ -45,6 +45,7 @@ const FILES = {
|
|
|
45
45
|
history: 'history.jsonl',
|
|
46
46
|
testResults: 'test-results',
|
|
47
47
|
nightLast: 'night-last.json',
|
|
48
|
+
nightNightly: 'night-nightly.json',
|
|
48
49
|
codeFindings: 'code-findings.json',
|
|
49
50
|
config: 'config.json',
|
|
50
51
|
snapshot: 'snapshot.json',
|
|
@@ -132,6 +133,7 @@ function pathsIn(dir) {
|
|
|
132
133
|
history: path.join(dir, FILES.history),
|
|
133
134
|
testResults: path.join(dir, FILES.testResults),
|
|
134
135
|
nightLast: path.join(dir, FILES.nightLast),
|
|
136
|
+
nightNightly: path.join(dir, FILES.nightNightly),
|
|
135
137
|
codeFindings: path.join(dir, FILES.codeFindings),
|
|
136
138
|
config: path.join(dir, FILES.config),
|
|
137
139
|
snapshot: path.join(dir, FILES.snapshot),
|
package/trouble.js
CHANGED
|
@@ -359,14 +359,19 @@ function explain(err, raw) {
|
|
|
359
359
|
const ref = code === 'ENOTFOUND' ? supabaseDirectRef(raw) : null;
|
|
360
360
|
if (ref) {
|
|
361
361
|
return [
|
|
362
|
-
'I could not find that server
|
|
362
|
+
'I could not find that server. There are two likely reasons.',
|
|
363
363
|
'',
|
|
364
|
-
'
|
|
365
|
-
'over IPv6. Plenty of home and office networks have no IPv6
|
|
366
|
-
'those the name does not resolve to anything
|
|
367
|
-
'happened here.',
|
|
364
|
+
'Most likely: that address is Supabase’s direct connection, and it now',
|
|
365
|
+
'answers only over IPv6. Plenty of home and office networks have no IPv6',
|
|
366
|
+
'at all, and on those the name does not resolve to anything.',
|
|
368
367
|
'',
|
|
369
|
-
|
|
368
|
+
// A ref with one letter wrong fails exactly the same way, and blaming
|
|
369
|
+
// only IPv6 would send somebody to change networks over a typo.
|
|
370
|
+
'Or the address is mistyped. Check that "' + ref + '" matches the project',
|
|
371
|
+
'ref in Supabase -> Project Settings -> General; one wrong letter fails',
|
|
372
|
+
'exactly the same way.',
|
|
373
|
+
'',
|
|
374
|
+
'Your password has not been tried yet, so it is not the problem. Use the',
|
|
370
375
|
'session pooler instead, which answers over the ordinary internet:',
|
|
371
376
|
'',
|
|
372
377
|
' Supabase dashboard -> Project Settings -> Database ->',
|