kryptheon-night 0.1.19 → 0.1.22
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 +68 -14
- package/finding.js +4 -1
- package/intro.js +13 -5
- package/package.json +3 -2
- package/scan.js +90 -28
- package/trouble.js +16 -2
package/bin/kryptheon-night.js
CHANGED
|
@@ -32,8 +32,8 @@ const intro = require('../intro.js');
|
|
|
32
32
|
const trouble = require('../trouble.js');
|
|
33
33
|
const { howToConnect } = require('../connect.js');
|
|
34
34
|
const installer = require('../installer.js');
|
|
35
|
-
const finding = require('../finding.js');
|
|
36
35
|
const outside = require('../outside.js');
|
|
36
|
+
const schema = require('../schema.js');
|
|
37
37
|
|
|
38
38
|
const line = (text) => process.stdout.write(text + '\n');
|
|
39
39
|
const fail = (text) => process.stderr.write(text + '\n');
|
|
@@ -88,7 +88,7 @@ function usage() {
|
|
|
88
88
|
// names a schema: `public` is the schema almost everybody has, and a bare
|
|
89
89
|
// word that is not one of these is still read as a schema so that
|
|
90
90
|
// `kryptheon-night public` keeps working.
|
|
91
|
-
const VERBS = ['install', 'uninstall', 'status', 'night'];
|
|
91
|
+
const VERBS = ['install', 'uninstall', 'status', 'night', 'words'];
|
|
92
92
|
|
|
93
93
|
/** What was asked for, and what was left to the default. */
|
|
94
94
|
function readArgs(argv) {
|
|
@@ -125,23 +125,43 @@ function readArgs(argv) {
|
|
|
125
125
|
* their answer already, and a folder that cannot be written is not theirs to
|
|
126
126
|
* debug here.
|
|
127
127
|
*/
|
|
128
|
-
function keepTheNight(
|
|
128
|
+
function keepTheNight(record) {
|
|
129
129
|
try {
|
|
130
|
-
scanner.saveNightly(scanner.nightlyFile(),
|
|
130
|
+
scanner.saveNightly(scanner.nightlyFile(), record);
|
|
131
131
|
} catch (err) {
|
|
132
132
|
/* the dashboard keeps its older copy, which carries its own date */
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The night's answer, made whole: its findings told with what their tables
|
|
138
|
+
* need for a right fix, plus the functions anyone can call - both read from
|
|
139
|
+
* the shape now, read only. Without the shape it is still the night's answer,
|
|
140
|
+
* just less exact.
|
|
141
|
+
*/
|
|
142
|
+
async function nightRecordFrom(client, latest) {
|
|
143
|
+
let plan = null;
|
|
144
|
+
const run = latest && latest.run;
|
|
145
|
+
if (run && !run.stopped) {
|
|
146
|
+
try {
|
|
147
|
+
plan = await schema.readSchema(client, run.source);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
plan = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return scanner.nightlyRecord(latest, new Date().toISOString(), plan);
|
|
153
|
+
}
|
|
154
|
+
|
|
136
155
|
/** Reads and keeps it, for the doors that do not already hold the answer. */
|
|
137
156
|
async function readAndKeepTheNight(client) {
|
|
138
157
|
try {
|
|
139
|
-
keepTheNight(await installer.latestRun(client, {}));
|
|
158
|
+
keepTheNight(await nightRecordFrom(client, await installer.latestRun(client, {})));
|
|
140
159
|
} catch (err) {
|
|
141
160
|
/* as above */
|
|
142
161
|
}
|
|
143
162
|
}
|
|
144
163
|
|
|
164
|
+
|
|
145
165
|
/**
|
|
146
166
|
* The three doors that are not a scan.
|
|
147
167
|
*
|
|
@@ -222,7 +242,8 @@ async function runVerb(command, client, target) {
|
|
|
222
242
|
|
|
223
243
|
if (command === 'night') {
|
|
224
244
|
const latest = await installer.latestRun(client, {});
|
|
225
|
-
|
|
245
|
+
const record = await nightRecordFrom(client, latest);
|
|
246
|
+
keepTheNight(record);
|
|
226
247
|
const where = latest.where;
|
|
227
248
|
if (!where) {
|
|
228
249
|
line(' The nightly run is not installed in this database, so there is');
|
|
@@ -239,20 +260,25 @@ async function runVerb(command, client, target) {
|
|
|
239
260
|
// product printed about a night that never happened.
|
|
240
261
|
line(' It is installed, and it has not run yet.');
|
|
241
262
|
line('');
|
|
242
|
-
line('
|
|
263
|
+
line(' It runs ' + scanner.scheduleTimes(where.job ? where.job.schedule : null) + '.');
|
|
243
264
|
line('');
|
|
244
265
|
return 0;
|
|
245
266
|
}
|
|
246
267
|
const run = latest.run;
|
|
247
|
-
line(' From the night of ' +
|
|
248
|
-
|
|
268
|
+
line(' From the night of ' + scanner.bothTimes(run.ran_at) + ', on "' + run.source + '".');
|
|
269
|
+
if (where.job) line(' It runs ' + scanner.scheduleTimes(where.job.schedule) + '.');
|
|
270
|
+
// The one part not from the night: the engine inside the database does
|
|
271
|
+
// not look at functions, so they are read now and said to be.
|
|
272
|
+
if (record.functionsReadAt) {
|
|
273
|
+
line(' Functions anyone can call were looked at just now, not overnight.');
|
|
274
|
+
}
|
|
249
275
|
scanner.report({
|
|
250
|
-
stopped:
|
|
251
|
-
attacksRun:
|
|
252
|
-
notChecked:
|
|
253
|
-
findings:
|
|
276
|
+
stopped: record.stopped,
|
|
277
|
+
attacksRun: record.attacksRun,
|
|
278
|
+
notChecked: record.notChecked || [],
|
|
279
|
+
findings: record.findings || [],
|
|
254
280
|
});
|
|
255
|
-
return run.stopped ? 2 : (
|
|
281
|
+
return run.stopped ? 2 : scanner.exitCodeFor(record);
|
|
256
282
|
}
|
|
257
283
|
|
|
258
284
|
throw new Error('there is no command called ' + command);
|
|
@@ -396,6 +422,23 @@ async function runOutside(appUrl) {
|
|
|
396
422
|
return result.findings.length ? 1 : 0;
|
|
397
423
|
}
|
|
398
424
|
|
|
425
|
+
/** Everything the dashboard needs to say what this command would say. */
|
|
426
|
+
function wordsFor(target, connection) {
|
|
427
|
+
const given = String(connection || '').trim();
|
|
428
|
+
const unusable = given ? trouble.readConnectionString(given) : null;
|
|
429
|
+
return {
|
|
430
|
+
help: intro.whereToFindIt(),
|
|
431
|
+
consent: intro.consentLines(target),
|
|
432
|
+
installConsent: intro.installConsentLines(installer.SCHEMA, target, installer.AT),
|
|
433
|
+
// pg_cron reads this in the database's own time zone, which is UTC on
|
|
434
|
+
// Supabase; the dashboard turns it into the person's local time.
|
|
435
|
+
nightlyAt: installer.AT,
|
|
436
|
+
given: Boolean(given),
|
|
437
|
+
unusable: unusable,
|
|
438
|
+
warning: given && !unusable ? trouble.poolerWarning(given) : null,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
399
442
|
/** Prints a block of plain lines with the indent the rest of the report uses. */
|
|
400
443
|
function block(lines, write) {
|
|
401
444
|
(write || line)('');
|
|
@@ -417,6 +460,17 @@ async function main() {
|
|
|
417
460
|
process.exit(2);
|
|
418
461
|
}
|
|
419
462
|
|
|
463
|
+
// The words this command says, as JSON, for the kryptheon dashboard to show
|
|
464
|
+
// on its own buttons: the help for finding the string, both consent
|
|
465
|
+
// screens, and - when KN_DATABASE_URL is given - what is wrong with it or
|
|
466
|
+
// worth warning about. Never connects, and never prints the string: the
|
|
467
|
+
// warnings name the host at most, and a string that cannot be used is
|
|
468
|
+
// described, not repeated.
|
|
469
|
+
if (asked.command === 'words') {
|
|
470
|
+
process.stdout.write(JSON.stringify(wordsFor(asked.schema || 'public', process.env.KN_DATABASE_URL || '')) + '\n');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
420
474
|
// The outside attack. Needs no connection string, no copy and no yes: it
|
|
421
475
|
// only asks the live app what it already answers any visitor, and asks for
|
|
422
476
|
// counts, never rows. So nothing below - the string, the consent screen,
|
package/finding.js
CHANGED
|
@@ -448,7 +448,10 @@ function bodyFor(finding, contents) {
|
|
|
448
448
|
}
|
|
449
449
|
return (
|
|
450
450
|
'Anyone on the internet, without logging in and without an account, can read ' +
|
|
451
|
-
|
|
451
|
+
// Read back the next morning, "just now" would be untrue: it was the
|
|
452
|
+
// nightly run, hours earlier.
|
|
453
|
+
'this table. ' + (finding.fromNight ? 'The nightly run did it on a copy and got back ' : 'I did it myself just now and got back ') +
|
|
454
|
+
finding.readable + ' ' + rowWord +
|
|
452
455
|
(holds ? ', including ' + holds : '') + '.'
|
|
453
456
|
);
|
|
454
457
|
}
|
package/intro.js
CHANGED
|
@@ -28,15 +28,23 @@ const trouble = require('./trouble.js');
|
|
|
28
28
|
* their users end up on the same page by a different door.
|
|
29
29
|
*/
|
|
30
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.
|
|
31
35
|
return [
|
|
32
36
|
'Where to find it:',
|
|
33
37
|
'',
|
|
34
|
-
' Supabase supabase.com/dashboard -> your project
|
|
35
|
-
'
|
|
36
|
-
'
|
|
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.',
|
|
37
45
|
'',
|
|
38
|
-
'
|
|
39
|
-
'
|
|
46
|
+
' Not port 6543 (the transaction pooler), and not the direct',
|
|
47
|
+
' db.<project>.supabase.co address - both are warned about.',
|
|
40
48
|
'',
|
|
41
49
|
' Lovable your app uses Supabase underneath. Open the Supabase',
|
|
42
50
|
' and Bolt project it made for you and follow the lines above.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon-night",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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 nightly.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 words.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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"check:recursion": "node recursion.check.js",
|
|
57
57
|
"check:guarded": "node guarded.check.js",
|
|
58
58
|
"check:nightly": "node nightly.check.js",
|
|
59
|
+
"check:words": "node words.check.js",
|
|
59
60
|
"check:cli": "node cli.check.js",
|
|
60
61
|
"check:writes": "node tamper.check.js",
|
|
61
62
|
"check:orphans": "node orphans.check.js",
|
package/scan.js
CHANGED
|
@@ -385,32 +385,12 @@ async function scan(client, sourceSchema, options) {
|
|
|
385
385
|
// version recorded only the flagged ones, so a revoke made the function
|
|
386
386
|
// vanish from both lists and the fix came back as "could not confirm".
|
|
387
387
|
for (const fn of plan.anonFunctions || []) attempted.push('privileged:' + fn.name);
|
|
388
|
-
|
|
389
|
-
// away before it does anything (schema.guardedAtTheDoor).
|
|
390
|
-
const privileged = (plan.anonFunctions || []).filter((fn) => fn.callable && !fn.aboutCaller && !fn.guarded).map((fn) => ({
|
|
391
|
-
kind: 'privileged',
|
|
392
|
-
fn: fn.name,
|
|
393
|
-
table: fn.name,
|
|
394
|
-
args: fn.args,
|
|
395
|
-
writes: fn.writes,
|
|
396
|
-
hasFixedSearchPath: fn.hasFixedSearchPath,
|
|
397
|
-
columns: [],
|
|
398
|
-
}));
|
|
388
|
+
const privileged = privilegedOf(plan);
|
|
399
389
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
const
|
|
404
|
-
// And whether a row says whose it is: with no such column, no rule can
|
|
405
|
-
// tie it to the person asking. Read from the copy, the way seeding did.
|
|
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)]));
|
|
410
|
-
const tagged = (f) => Object.assign({}, f,
|
|
411
|
-
ties.has(f.table) ? { authTied: ties.get(f.table) } : {},
|
|
412
|
-
owned.has(f.table) ? { owned: Boolean(owned.get(f.table)) } : {},
|
|
413
|
-
owned.get(f.table) ? { ownedBy: owned.get(f.table) } : {});
|
|
390
|
+
// Ties read from the original schema: the copy points auth.users keys at
|
|
391
|
+
// a stand-in, and would call every table untied. Owners read from the
|
|
392
|
+
// copy, the way seeding did.
|
|
393
|
+
const tagged = taggerFor(plan, theirs);
|
|
414
394
|
|
|
415
395
|
return {
|
|
416
396
|
stopped: null,
|
|
@@ -442,6 +422,75 @@ function exitCodeFor(result) {
|
|
|
442
422
|
return (result.findings || []).some((f) => f.status !== 'verification required') ? 1 : 0;
|
|
443
423
|
}
|
|
444
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Functions an anonymous visitor can call that run past the rules. Read from
|
|
427
|
+
* the shape, not attacked. Quiet: a yes/no about the caller, and a function
|
|
428
|
+
* that turns the caller away before it does anything (schema.guardedAtTheDoor).
|
|
429
|
+
*/
|
|
430
|
+
function privilegedOf(plan) {
|
|
431
|
+
return ((plan && plan.anonFunctions) || []).filter((fn) => fn.callable && !fn.aboutCaller && !fn.guarded).map((fn) => ({
|
|
432
|
+
kind: 'privileged',
|
|
433
|
+
fn: fn.name,
|
|
434
|
+
table: fn.name,
|
|
435
|
+
args: fn.args,
|
|
436
|
+
writes: fn.writes,
|
|
437
|
+
hasFixedSearchPath: fn.hasFixedSearchPath,
|
|
438
|
+
columns: [],
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* What each finding needs to know about its table for its fix to be right:
|
|
444
|
+
* whether the app signs people in through Supabase, whether a row says whose
|
|
445
|
+
* it is, and which column that is. Without these a prompt falls back to "the
|
|
446
|
+
* column that says who each row belongs to" - found on a nightly read-back,
|
|
447
|
+
* whose findings came from the database without them.
|
|
448
|
+
*/
|
|
449
|
+
function taggerFor(plan, tables) {
|
|
450
|
+
const ties = finding.authTiesOf(plan);
|
|
451
|
+
const owned = new Map((tables || []).map((table) => [table.name, attack.ownerColumn(table)]));
|
|
452
|
+
return (f) => Object.assign({}, f,
|
|
453
|
+
ties.has(f.table) ? { authTied: ties.get(f.table) } : {},
|
|
454
|
+
owned.has(f.table) ? { owned: Boolean(owned.get(f.table)) } : {},
|
|
455
|
+
owned.get(f.table) ? { ownedBy: owned.get(f.table) } : {});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* What the nightly engine wrote down as not tried, in the shape a scan uses.
|
|
460
|
+
* It names a column's check as table and column apart, with no attack named,
|
|
461
|
+
* so "export_jobs.access_token could not be raced" was counted as a whole
|
|
462
|
+
* table not checked - and listed without its column.
|
|
463
|
+
*/
|
|
464
|
+
function notCheckedFromNight(entries) {
|
|
465
|
+
return (entries || []).map((e) => {
|
|
466
|
+
if (!e || e.key || !e.column) return e;
|
|
467
|
+
const kind = /two requests at the same instant/.test(String(e.why)) ? 'duplicated' : 'orphaned';
|
|
468
|
+
return { table: e.table + '.' + e.column, key: kind + ':' + e.table + ':' + e.column, why: e.why };
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* A moment of the night, said in UTC - which is what the database runs on -
|
|
474
|
+
* and in this computer's own time, so 03:00 is not read as three at night
|
|
475
|
+
* here. Found on a real read-back: "the night of 2026-10-01 03:00" was 08:30
|
|
476
|
+
* in India, and nothing said so.
|
|
477
|
+
*/
|
|
478
|
+
function bothTimes(iso) {
|
|
479
|
+
const at = new Date(iso);
|
|
480
|
+
return at.toISOString().slice(0, 16).replace('T', ' ') + ' UTC (' +
|
|
481
|
+
at.toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) + ' your time)';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** "0 3 * * *" as a time of day in both, or the schedule as written. */
|
|
485
|
+
function scheduleTimes(cron) {
|
|
486
|
+
const m = /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/.exec(String(cron || '').trim());
|
|
487
|
+
if (!m) return String(cron || 'whenever it is scheduled');
|
|
488
|
+
const now = new Date();
|
|
489
|
+
const at = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), Number(m[2]), Number(m[1])));
|
|
490
|
+
return 'every night at ' + at.toISOString().slice(11, 16) + ' UTC (' +
|
|
491
|
+
at.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) + ' your time)';
|
|
492
|
+
}
|
|
493
|
+
|
|
445
494
|
/** Where the nightly run's last answer is kept for the dashboard. */
|
|
446
495
|
function nightlyFile() {
|
|
447
496
|
return store.open(process.cwd()).nightNightly;
|
|
@@ -453,7 +502,7 @@ function nightlyFile() {
|
|
|
453
502
|
* as "nothing found" - not installed, installed and never run, and stopped -
|
|
454
503
|
* said as themselves. `latest` is installer.latestRun's answer.
|
|
455
504
|
*/
|
|
456
|
-
function nightlyRecord(latest, readAt) {
|
|
505
|
+
function nightlyRecord(latest, readAt, plan) {
|
|
457
506
|
const at = (value) => (value ? new Date(value).toISOString() : null);
|
|
458
507
|
if (!latest || !latest.installed) return { readAt: readAt, installed: false };
|
|
459
508
|
const where = latest.where || {};
|
|
@@ -467,13 +516,21 @@ function nightlyRecord(latest, readAt) {
|
|
|
467
516
|
};
|
|
468
517
|
const run = latest.run;
|
|
469
518
|
if (!run) return record;
|
|
519
|
+
// The night's findings, told as the night's: tagged with what their table
|
|
520
|
+
// needs for a right fix when the schema could be read, and joined by the
|
|
521
|
+
// functions anyone can call - read now, when this is asked, because the
|
|
522
|
+
// engine inside the database does not look at them.
|
|
523
|
+
const tag = plan ? taggerFor(plan, plan.tables) : (f) => f;
|
|
524
|
+
const night = (run.findings || []).map((f) => tag(Object.assign({}, f, { fromNight: true })));
|
|
525
|
+
const now = plan ? privilegedOf(plan).map(tag) : [];
|
|
470
526
|
return Object.assign(record, {
|
|
471
527
|
ranAt: at(run.ran_at),
|
|
472
528
|
source: run.source,
|
|
473
529
|
stopped: run.stopped || null,
|
|
474
530
|
attacksRun: run.attacks_run || 0,
|
|
475
|
-
notChecked: run.not_checked
|
|
476
|
-
findings: run.stopped ? [] : finding.describeAll(
|
|
531
|
+
notChecked: notCheckedFromNight(run.not_checked),
|
|
532
|
+
findings: run.stopped ? [] : finding.describeAll(night.concat(now)),
|
|
533
|
+
functionsReadAt: plan ? readAt : null,
|
|
477
534
|
});
|
|
478
535
|
}
|
|
479
536
|
|
|
@@ -781,6 +838,11 @@ module.exports = {
|
|
|
781
838
|
saveRun: saveRun,
|
|
782
839
|
nightlyFile: nightlyFile,
|
|
783
840
|
nightlyRecord: nightlyRecord,
|
|
841
|
+
bothTimes: bothTimes,
|
|
842
|
+
scheduleTimes: scheduleTimes,
|
|
843
|
+
notCheckedFromNight: notCheckedFromNight,
|
|
844
|
+
privilegedOf: privilegedOf,
|
|
845
|
+
taggerFor: taggerFor,
|
|
784
846
|
saveNightly: saveNightly,
|
|
785
847
|
sweepOldCopies: sweepOldCopies,
|
|
786
848
|
notTestedLines: notTestedLines,
|
package/trouble.js
CHANGED
|
@@ -189,8 +189,22 @@ function poolerWarning(raw) {
|
|
|
189
189
|
'That is the transaction pooler (port 6543). It gives every statement a',
|
|
190
190
|
'different connection, and these attacks have to stay on one.',
|
|
191
191
|
'',
|
|
192
|
-
'Use the session pooler
|
|
193
|
-
'
|
|
192
|
+
'Use the session pooler instead: Supabase -> Connect -> Session pooler,',
|
|
193
|
+
'the one whose port is 5432.',
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The direct connection answers only over IPv6 now. On a network without
|
|
198
|
+
// it the name does not resolve, and the failure reads like a typo. It works
|
|
199
|
+
// where IPv6 does, so this warns rather than refuses.
|
|
200
|
+
if (/^db\.[a-z0-9]+\.supabase\.(co|com)$/i.test(url.hostname)) {
|
|
201
|
+
return [
|
|
202
|
+
'That is Supabase\'s direct connection (db.' + url.hostname.split('.')[1] + '.supabase.co). It',
|
|
203
|
+
'answers only over IPv6, and many home and office networks have none - it',
|
|
204
|
+
'then fails as if the address were wrong.',
|
|
205
|
+
'',
|
|
206
|
+
'If it does, use the session pooler: Supabase -> Connect -> Session pooler,',
|
|
207
|
+
'port 5432.',
|
|
194
208
|
];
|
|
195
209
|
}
|
|
196
210
|
|