kryptheon-night 0.1.30 → 0.1.31
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/README.md +8 -0
- package/installer.js +232 -0
- package/intro.js +30 -6
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -167,6 +167,14 @@ properly.
|
|
|
167
167
|
`uninstall` removes the job, the schema, and any extension it had to add, and
|
|
168
168
|
leaves alone anything that was already there.
|
|
169
169
|
|
|
170
|
+
**No terminal at all?** Your dashboard on kryptheon.tech gives you the same
|
|
171
|
+
install as one SQL script: paste it into your Supabase SQL editor and press
|
|
172
|
+
Run. It leaves your database exactly as `npx kryptheon-night install` does —
|
|
173
|
+
`installscript.check.js` installs both ways side by side and compares every
|
|
174
|
+
function, the job, the token and the record uninstall reads — and the matching
|
|
175
|
+
uninstall script takes it back out, whichever way it went in. From code:
|
|
176
|
+
`require('kryptheon-night/installer.js').installScript({ source, reportTo })`.
|
|
177
|
+
|
|
170
178
|
## Three answers, three exit codes
|
|
171
179
|
|
|
172
180
|
| exit | meaning |
|
package/installer.js
CHANGED
|
@@ -213,6 +213,234 @@ async function configureReporting(client, schema, reportTo) {
|
|
|
213
213
|
return { endpoint: reportTo.endpoint };
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/*
|
|
217
|
+
* The same install, as one SQL script a person pastes into their Supabase SQL
|
|
218
|
+
* editor and runs - for somebody who never opens a terminal. It has to leave
|
|
219
|
+
* the database exactly as install() does (installscript.check.js holds the
|
|
220
|
+
* two side by side), so it does the same things in the same order and writes
|
|
221
|
+
* down the same record for uninstall to read.
|
|
222
|
+
*
|
|
223
|
+
* Every value a person supplies - the schema their app is in, the project
|
|
224
|
+
* token, where the summary goes - appears once, in the settings at the top,
|
|
225
|
+
* as one plain SQL string. The rest of the script reads them from there. That
|
|
226
|
+
* is what lets kryptheon.tech fill the script in a browser without knowing
|
|
227
|
+
* any SQL: it replaces three markers, each inside a single string.
|
|
228
|
+
*/
|
|
229
|
+
const SLOTS = {
|
|
230
|
+
source: 'KN_SLOT_SOURCE',
|
|
231
|
+
token: 'KN_SLOT_TOKEN',
|
|
232
|
+
endpoint: 'KN_SLOT_ENDPOINT',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function engineName() {
|
|
236
|
+
try {
|
|
237
|
+
const manifest = JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8'));
|
|
238
|
+
return 'kryptheon-night ' + manifest.version;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
return 'kryptheon-night';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The script with its three markers in place, for a page to fill. Without a
|
|
246
|
+
* token the slot is filled with nothing, and the script installs the plain
|
|
247
|
+
* night that sends nothing anywhere.
|
|
248
|
+
*/
|
|
249
|
+
function installTemplate(options) {
|
|
250
|
+
const opts = options || {};
|
|
251
|
+
const schema = opts.schema || SCHEMA;
|
|
252
|
+
const at = opts.at || AT;
|
|
253
|
+
const engine = opts.engine || engineName();
|
|
254
|
+
const q = quote(schema);
|
|
255
|
+
const lit = (v) => literal(v);
|
|
256
|
+
return [
|
|
257
|
+
'-- Kryptheon: the nightly check, set up from inside your database.',
|
|
258
|
+
'-- ' + engine + '. Paste all of this into the SQL editor of the project',
|
|
259
|
+
'-- you want watched, and press Run. If Supabase warns about a destructive',
|
|
260
|
+
'-- operation: it means the "DROP ... IF EXISTS" lines below, which only ever',
|
|
261
|
+
'-- touch what Kryptheon itself made.',
|
|
262
|
+
'--',
|
|
263
|
+
'-- What it makes: a schema called ' + schema + ' with the checking functions in it,',
|
|
264
|
+
'-- pg_cron and pg_net if they are not here yet, and one job, kryptheon_nightly,',
|
|
265
|
+
'-- at ' + at + ' (UTC on Supabase). It never reads a row of your data.',
|
|
266
|
+
'--',
|
|
267
|
+
'-- To take it all away again: the uninstall script on kryptheon.tech, or',
|
|
268
|
+
'-- npx kryptheon-night uninstall',
|
|
269
|
+
'',
|
|
270
|
+
'-- Your settings. Only these three lines are yours; the rest is the same for everybody.',
|
|
271
|
+
'DROP TABLE IF EXISTS pg_temp.kn_install;',
|
|
272
|
+
'CREATE TEMP TABLE kn_install (source text NOT NULL, token text, endpoint text, made_extensions text[] NOT NULL DEFAULT \'{}\', made_schema boolean);',
|
|
273
|
+
'INSERT INTO kn_install (source, token, endpoint) VALUES (',
|
|
274
|
+
" '" + SLOTS.source + "', -- the schema your app's tables are in",
|
|
275
|
+
" '" + SLOTS.token + "', -- your project token; empty for no nightly summary",
|
|
276
|
+
" '" + SLOTS.endpoint + "' -- where the summary goes",
|
|
277
|
+
');',
|
|
278
|
+
"UPDATE kn_install SET token = nullif(token, ''), endpoint = nullif(endpoint, '');",
|
|
279
|
+
'',
|
|
280
|
+
'-- Refused before anything is made: a token that is not one, an address that',
|
|
281
|
+
'-- is not Kryptheon\'s, or a database without the two extensions.',
|
|
282
|
+
'DO $kn$',
|
|
283
|
+
'DECLARE s record;',
|
|
284
|
+
'BEGIN',
|
|
285
|
+
' SELECT * INTO s FROM kn_install;',
|
|
286
|
+
" IF s.source = '' THEN RAISE EXCEPTION 'Kryptheon: name the schema your app is in (it is \"public\" for almost everyone)'; END IF;",
|
|
287
|
+
' IF s.token IS NOT NULL OR s.endpoint IS NOT NULL THEN',
|
|
288
|
+
" IF s.token IS NULL OR s.token !~ '^kp_[A-Za-z0-9_-]{20,100}$' THEN",
|
|
289
|
+
" RAISE EXCEPTION 'Kryptheon: that is not a project token - copy it again from your dashboard on kryptheon.tech';",
|
|
290
|
+
' END IF;',
|
|
291
|
+
" IF s.endpoint IS NULL OR s.endpoint !~* '^https://[a-z0-9-]+(\\.[a-z0-9-]+)+(:443)?/functions/v1/ingest$' THEN",
|
|
292
|
+
" RAISE EXCEPTION 'Kryptheon: the report address has to be an https address ending in /functions/v1/ingest';",
|
|
293
|
+
' END IF;',
|
|
294
|
+
' END IF;',
|
|
295
|
+
" IF NOT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pg_net') THEN",
|
|
296
|
+
" RAISE EXCEPTION 'Kryptheon: this database does not offer pg_net, so the nightly run cannot be installed. Supabase has it; a plain Postgres may not.';",
|
|
297
|
+
' END IF;',
|
|
298
|
+
" IF NOT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pg_cron') THEN",
|
|
299
|
+
" RAISE EXCEPTION 'Kryptheon: this database does not offer pg_cron, so the nightly run cannot be installed. Supabase has it; a plain Postgres may not.';",
|
|
300
|
+
' END IF;',
|
|
301
|
+
' -- pg_net first, and told where to go: left to itself it lands in your own public schema.',
|
|
302
|
+
" IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net') THEN",
|
|
303
|
+
" IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'extensions') THEN",
|
|
304
|
+
' CREATE EXTENSION pg_net WITH SCHEMA extensions;',
|
|
305
|
+
' ELSE',
|
|
306
|
+
' CREATE EXTENSION pg_net;',
|
|
307
|
+
' END IF;',
|
|
308
|
+
" UPDATE kn_install SET made_extensions = array_append(made_extensions, 'pg_net');",
|
|
309
|
+
' END IF;',
|
|
310
|
+
" IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron') THEN",
|
|
311
|
+
' CREATE EXTENSION pg_cron;',
|
|
312
|
+
" UPDATE kn_install SET made_extensions = array_append(made_extensions, 'pg_cron');",
|
|
313
|
+
' END IF;',
|
|
314
|
+
' UPDATE kn_install SET made_schema = NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ' + lit(schema) + ');',
|
|
315
|
+
'END $kn$;',
|
|
316
|
+
'',
|
|
317
|
+
'-- The checking functions.',
|
|
318
|
+
sqlengine.engineFor(schema),
|
|
319
|
+
'',
|
|
320
|
+
'-- What this install made, where uninstall will look for it.',
|
|
321
|
+
'CREATE TABLE IF NOT EXISTS ' + q + '.installed (' +
|
|
322
|
+
'id integer PRIMARY KEY DEFAULT 1, ' +
|
|
323
|
+
'installed_at timestamptz NOT NULL DEFAULT now(), ' +
|
|
324
|
+
'source text NOT NULL, ' +
|
|
325
|
+
'made_extensions text[] NOT NULL DEFAULT \'{}\', ' +
|
|
326
|
+
'made_schema boolean NOT NULL DEFAULT false, ' +
|
|
327
|
+
'jobid bigint, ' +
|
|
328
|
+
'CONSTRAINT only_one CHECK (id = 1));',
|
|
329
|
+
'',
|
|
330
|
+
'-- The project token, kept where only the database owner can read it - never',
|
|
331
|
+
'-- anon or authenticated, the roles your app\'s API runs as. With no token,',
|
|
332
|
+
'-- an earlier one is taken away and nothing is ever sent.',
|
|
333
|
+
'DO $kn$',
|
|
334
|
+
'DECLARE s record; r text;',
|
|
335
|
+
'BEGIN',
|
|
336
|
+
' SELECT * INTO s FROM kn_install;',
|
|
337
|
+
' IF s.token IS NULL THEN',
|
|
338
|
+
' DROP TABLE IF EXISTS ' + q + '.reporting;',
|
|
339
|
+
' RETURN;',
|
|
340
|
+
' END IF;',
|
|
341
|
+
' CREATE TABLE IF NOT EXISTS ' + q + '.reporting (' +
|
|
342
|
+
'id integer PRIMARY KEY DEFAULT 1, endpoint text NOT NULL, token text NOT NULL, engine text, ' +
|
|
343
|
+
'CONSTRAINT only_one CHECK (id = 1));',
|
|
344
|
+
' REVOKE ALL ON ' + q + '.reporting FROM PUBLIC;',
|
|
345
|
+
" FOREACH r IN ARRAY ARRAY['anon', 'authenticated'] LOOP",
|
|
346
|
+
" IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = r) THEN EXECUTE format('REVOKE ALL ON %s.reporting FROM %I', " + lit(q) + ', r); END IF;',
|
|
347
|
+
' END LOOP;',
|
|
348
|
+
' INSERT INTO ' + q + '.reporting (id, endpoint, token, engine) VALUES (1, s.endpoint, s.token, ' + lit(engine) + ')',
|
|
349
|
+
' ON CONFLICT (id) DO UPDATE SET endpoint = EXCLUDED.endpoint, token = EXCLUDED.token, engine = EXCLUDED.engine;',
|
|
350
|
+
'END $kn$;',
|
|
351
|
+
'',
|
|
352
|
+
'-- The nightly job, replaced rather than added to, and the record of it all.',
|
|
353
|
+
'DO $kn$',
|
|
354
|
+
'DECLARE s record; job bigint;',
|
|
355
|
+
'BEGIN',
|
|
356
|
+
' SELECT * INTO s FROM kn_install;',
|
|
357
|
+
" job := cron.schedule('kryptheon_nightly', " + lit(at) + ',',
|
|
358
|
+
" 'SELECT ' || " + lit(q) + " || '.' || CASE WHEN s.token IS NULL THEN 'nightly' ELSE 'nightly_and_report' END ||",
|
|
359
|
+
" '(''' || replace(s.source, '''', '''''') || ''')');",
|
|
360
|
+
' INSERT INTO ' + q + '.installed (id, source, made_extensions, made_schema, jobid)',
|
|
361
|
+
' VALUES (1, s.source, s.made_extensions, s.made_schema, job)',
|
|
362
|
+
' ON CONFLICT (id) DO UPDATE SET source = EXCLUDED.source, jobid = EXCLUDED.jobid,',
|
|
363
|
+
' made_extensions = ' + q + '.installed.made_extensions || EXCLUDED.made_extensions, installed_at = now();',
|
|
364
|
+
'END $kn$;',
|
|
365
|
+
'',
|
|
366
|
+
"SELECT 'Installed. Kryptheon will check \"' || source || '\" every night at " + at + " (UTC)' ||",
|
|
367
|
+
" CASE WHEN token IS NULL THEN ', and sends nothing anywhere.' ELSE ', and sends your Kryptheon dashboard a summary - never a row of data.' END AS kryptheon",
|
|
368
|
+
' FROM kn_install;',
|
|
369
|
+
'DROP TABLE kn_install;',
|
|
370
|
+
'',
|
|
371
|
+
].join('\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* uninstall() as one pasted script: the job first, then the schema, then only
|
|
376
|
+
* the extensions the install wrote down as its own. Reads the same record the
|
|
377
|
+
* CLI does, so it takes back an install made either way.
|
|
378
|
+
*/
|
|
379
|
+
function uninstallScript(options) {
|
|
380
|
+
const schema = (options || {}).schema || SCHEMA;
|
|
381
|
+
const q = quote(schema);
|
|
382
|
+
return [
|
|
383
|
+
'-- Kryptheon: take the nightly check away again. Paste all of this into the SQL',
|
|
384
|
+
'-- editor of the project it watches, and press Run. It removes the job, the',
|
|
385
|
+
'-- ' + schema + ' schema, and any extension Kryptheon itself added - and leaves',
|
|
386
|
+
'-- alone anything that was already here.',
|
|
387
|
+
'DO $kn$',
|
|
388
|
+
'DECLARE job bigint; made_schema boolean; made text[]; e text;',
|
|
389
|
+
'BEGIN',
|
|
390
|
+
' IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ' + literal(schema) + ') THEN',
|
|
391
|
+
" RAISE NOTICE 'Kryptheon is not installed here. Nothing was changed.';",
|
|
392
|
+
' RETURN;',
|
|
393
|
+
' END IF;',
|
|
394
|
+
' BEGIN',
|
|
395
|
+
' EXECUTE ' + literal('SELECT jobid, made_schema, made_extensions FROM ' + q + '.installed WHERE id = 1') + ' INTO job, made_schema, made;',
|
|
396
|
+
' EXCEPTION WHEN OTHERS THEN',
|
|
397
|
+
' job := NULL; made_schema := NULL; made := NULL; -- no record: the schema goes, nothing else',
|
|
398
|
+
' END;',
|
|
399
|
+
' IF job IS NOT NULL THEN',
|
|
400
|
+
' BEGIN',
|
|
401
|
+
' PERFORM cron.unschedule(job);',
|
|
402
|
+
' EXCEPTION WHEN OTHERS THEN NULL;',
|
|
403
|
+
' END;',
|
|
404
|
+
" IF to_regclass('cron.job') IS NOT NULL AND EXISTS (SELECT 1 FROM cron.job j WHERE j.jobid = job) THEN",
|
|
405
|
+
" RAISE EXCEPTION 'Kryptheon: the nightly job % is still scheduled, so nothing else was removed', job;",
|
|
406
|
+
' END IF;',
|
|
407
|
+
' END IF;',
|
|
408
|
+
' DROP SCHEMA IF EXISTS ' + q + ' CASCADE;',
|
|
409
|
+
" FOREACH e IN ARRAY coalesce(made, '{}') LOOP",
|
|
410
|
+
' BEGIN',
|
|
411
|
+
" EXECUTE 'DROP EXTENSION IF EXISTS ' || e;",
|
|
412
|
+
' EXCEPTION WHEN OTHERS THEN NULL;',
|
|
413
|
+
' END;',
|
|
414
|
+
' END LOOP;',
|
|
415
|
+
'END $kn$;',
|
|
416
|
+
'',
|
|
417
|
+
"SELECT 'Kryptheon is gone from this database: no job, no schema, and only the extensions it added were removed.' AS kryptheon;",
|
|
418
|
+
'',
|
|
419
|
+
].join('\n');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** A value, ready to stand where a marker stood inside a SQL string. */
|
|
423
|
+
function fillSlot(value) {
|
|
424
|
+
return String(value == null ? '' : value).split("'").join("''");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The script, filled in. Refuses a bad token the way install() does, before
|
|
429
|
+
* there is anything to paste.
|
|
430
|
+
*/
|
|
431
|
+
function installScript(options) {
|
|
432
|
+
const opts = options || {};
|
|
433
|
+
if (opts.reportTo) checkReportTo(opts.reportTo);
|
|
434
|
+
const report = opts.reportTo || {};
|
|
435
|
+
const values = { source: opts.source || 'public', token: report.token, endpoint: report.endpoint };
|
|
436
|
+
let sql = installTemplate({ schema: opts.schema, at: opts.at, engine: report.engine });
|
|
437
|
+
for (const [slot, marker] of Object.entries(SLOTS)) {
|
|
438
|
+
if (sql.split(marker).length !== 2) throw new Error('the install script carries ' + marker + ' more or less than once');
|
|
439
|
+
sql = sql.split(marker).join(fillSlot(values[slot]));
|
|
440
|
+
}
|
|
441
|
+
return sql;
|
|
442
|
+
}
|
|
443
|
+
|
|
216
444
|
/** A string, quoted the way Postgres wants it inside another statement. */
|
|
217
445
|
function literal(text) {
|
|
218
446
|
return "'" + String(text).split("'").join("''") + "'";
|
|
@@ -314,4 +542,8 @@ module.exports = {
|
|
|
314
542
|
extensionState: extensionState,
|
|
315
543
|
configureReporting: configureReporting,
|
|
316
544
|
jobCommand: jobCommand,
|
|
545
|
+
installTemplate: installTemplate,
|
|
546
|
+
installScript: installScript,
|
|
547
|
+
uninstallScript: uninstallScript,
|
|
548
|
+
SLOTS: SLOTS,
|
|
317
549
|
};
|
package/intro.js
CHANGED
|
@@ -174,11 +174,20 @@ function installConsentLines(schema, source, at, reporting) {
|
|
|
174
174
|
];
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/**
|
|
178
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Asks a question and hands back what was typed - or null when there is no
|
|
179
|
+
* more input to read, so a closed keyboard is an answer and not a wait forever.
|
|
180
|
+
*/
|
|
181
|
+
function ask(question, io) {
|
|
182
|
+
const streams = io || { input: process.stdin, output: process.stdout };
|
|
179
183
|
return new Promise((resolve) => {
|
|
180
|
-
const rl = readline.createInterface({ input:
|
|
184
|
+
const rl = readline.createInterface({ input: streams.input, output: streams.output });
|
|
185
|
+
let answered = false;
|
|
186
|
+
rl.on('close', () => {
|
|
187
|
+
if (!answered) resolve(null);
|
|
188
|
+
});
|
|
181
189
|
rl.question(question, (answer) => {
|
|
190
|
+
answered = true;
|
|
182
191
|
rl.close();
|
|
183
192
|
resolve(String(answer || '').trim());
|
|
184
193
|
});
|
|
@@ -235,9 +244,24 @@ function isYes(answer) {
|
|
|
235
244
|
return said === 'y' || said === 'yes';
|
|
236
245
|
}
|
|
237
246
|
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Yes or no, where anything that is not a clear yes is a no - except an empty
|
|
249
|
+
* line, which is asked again.
|
|
250
|
+
*
|
|
251
|
+
* On Windows a pasted connection string can carry its own line ending: the
|
|
252
|
+
* hidden prompt takes it and moves on, and the Enter the person then presses
|
|
253
|
+
* to "send" the paste lands here, on a question they have not read yet. Taken
|
|
254
|
+
* as a no, it stopped an install nobody had said no to. An empty line is
|
|
255
|
+
* still never a yes; it is only not an answer.
|
|
256
|
+
*/
|
|
257
|
+
async function askYesNo(question, io) {
|
|
258
|
+
const streams = io || { input: process.stdin, output: process.stdout };
|
|
259
|
+
for (;;) {
|
|
260
|
+
const answer = await ask(question, streams);
|
|
261
|
+
if (answer === null) return false; // nothing more can be typed
|
|
262
|
+
if (answer !== '') return isYes(answer);
|
|
263
|
+
streams.output.write(' Type y and press Enter to go ahead, or n to stop.\n');
|
|
264
|
+
}
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon-night",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
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 words.check.js && node helpers.check.js && node deeploop.check.js && node teams.check.js && node roles.check.js && node buckets.check.js && node cli.check.js && node loop.check.js && node report.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 helpers.check.js && node deeploop.check.js && node teams.check.js && node roles.check.js && node buckets.check.js && node cli.check.js && node loop.check.js && node report.check.js && node installscript.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",
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"check:engines": "node engines.check.js",
|
|
75
75
|
"check:installer": "node installer.check.js",
|
|
76
76
|
"check:roles": "node roles.check.js",
|
|
77
|
-
"check:buckets": "node buckets.check.js"
|
|
77
|
+
"check:buckets": "node buckets.check.js",
|
|
78
|
+
"check:installscript": "node installscript.check.js"
|
|
78
79
|
},
|
|
79
80
|
"keywords": [
|
|
80
81
|
"supabase",
|