kryptheon-night 0.1.32 → 0.1.34

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/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ Kryptheon Software License
2
+
3
+ Copyright (c) 2026 Kryptheon (https://kryptheon.tech). All rights reserved.
4
+
5
+ This software is proprietary. It is not open source.
6
+
7
+ 1. Use. You may install and run this software, as published by Kryptheon,
8
+ free of charge, to check software and databases that you own or are
9
+ authorised to test. Features that kryptheon.tech offers on a paid plan
10
+ are provided only under that plan and the Terms at
11
+ https://kryptheon.tech/terms.
12
+
13
+ 2. Restrictions. Without Kryptheon's written permission you may not:
14
+ (a) copy, modify, translate or create derivative works of this software,
15
+ except as needed to run it;
16
+ (b) sell, sublicense, rent, publish or otherwise distribute it, or offer
17
+ it, or anything derived from it, as a hosted or managed service;
18
+ (c) remove, disable or work around any licence, plan or usage limit, or
19
+ any part of the software that enforces them;
20
+ (d) use it, or any part of it, to build a competing product.
21
+
22
+ 3. No warranty. The software is provided "as is", without warranty of any
23
+ kind, express or implied, including fitness for a particular purpose.
24
+ It may miss problems; a check that finds nothing does not mean that
25
+ nothing is wrong. You are responsible for what you test and for keeping
26
+ your own backups.
27
+
28
+ 4. Limitation of liability. To the extent the law allows, Kryptheon is not
29
+ liable for any damages arising from the use of, or inability to use,
30
+ this software.
31
+
32
+ 5. Earlier versions. Versions published before this licence was added keep
33
+ the licence they were published with.
34
+
35
+ 6. Termination. This licence ends automatically if you break it. When it
36
+ ends, you must stop using the software and delete your copies.
37
+
38
+ Questions: company@kryptheon.tech
package/README.md CHANGED
@@ -244,3 +244,7 @@ and it has not changed.
244
244
 
245
245
  If your network blocks outbound 5432, set `KN_PRELOAD` to a module that swaps
246
246
  the driver for one reaching Postgres over 443.
247
+
248
+ ## Licence
249
+
250
+ Free to use, not open source: see LICENSE. Paid features are on kryptheon.tech.
package/attack.js CHANGED
@@ -204,6 +204,132 @@ function allowedByCheck(table, columnName) {
204
204
  return found;
205
205
  }
206
206
 
207
+ /**
208
+ * The CHECK constraints on a table that name this column, as the expression
209
+ * inside CHECK (...). A NOT VALID one still applies to every new row.
210
+ */
211
+ function checksOn(table, columnName) {
212
+ const escaped = String(columnName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
213
+ const named = new RegExp('(^|[^A-Za-z0-9_$"])("' + escaped.split('"').join('""') + '"|' + escaped + ')($|[^A-Za-z0-9_$"])');
214
+ const found = [];
215
+ for (const constraint of table.constraints || []) {
216
+ if (constraint.kind !== 'c') continue;
217
+ const definition = String(constraint.definition || '');
218
+ if (!named.test(definition)) continue;
219
+ found.push(definition.replace(/^CHECK\s*/i, '').replace(/\s+NOT VALID\s*$/i, ''));
220
+ }
221
+ return found;
222
+ }
223
+
224
+ const NUMERIC_KIND = /^(integer|bigint|smallint|numeric|decimal|real|double)/;
225
+ const TEXT_KIND = /^(text|character|varchar|citext)/;
226
+
227
+ /**
228
+ * Values to offer a column whose CHECK could not be read, best first.
229
+ *
230
+ * Found on a real app (a clinic's, 2026-10-11): patient_waitlist and
231
+ * whatsapp_contacts each had a phone number column with a CHECK on its
232
+ * format, compliance_requests a type column with a CHECK no reading here
233
+ * understood - and none of the made-up values fitted, so three tables, and a
234
+ * fourth that pointed at one of them, were never tested. Nothing here reads
235
+ * the rule. Each value is put to the rule itself, in the database
236
+ * (fittingValue), and the first it accepts is used.
237
+ *
238
+ * Mirrored by __KN__.check_candidates in engine.sql, value for value and in
239
+ * the same order: both engines must seed the same rows.
240
+ */
241
+ function checkCandidates(column, definitions, distinct) {
242
+ const kind = String(column.base_type || column.type).toLowerCase();
243
+ const tag = distinct === undefined || distinct === null ? '' : String(distinct);
244
+ const step = Number(tag) || 0;
245
+ const out = [];
246
+ const add = (value) => { if (out.indexOf(value) === -1) out.push(value); };
247
+ if (NUMERIC_KIND.test(kind)) {
248
+ [1 + step, 18 + step, 100 + step, 1000 + step, 0, 5].forEach((n) => add(String(n)));
249
+ for (const definition of definitions) {
250
+ const numbers = definition.match(/-?\d+(?:\.\d+)?/g) || [];
251
+ numbers.forEach(add);
252
+ }
253
+ return out;
254
+ }
255
+ if (!TEXT_KIND.test(kind)) return out;
256
+ const d = String(step % 10);
257
+ const phones = ['+1415555012' + d, '+919876543' + (210 + step), '9876543' + (210 + step), '415555012' + d,
258
+ '+1 415-555-012' + d, '(415) 555-012' + d, '415-555-012' + d, '+44 7700 90012' + d, '07700 90012' + d];
259
+ const emails = ['kryptheon' + tag + '@example.com'];
260
+ const links = ['https://example.com/kryptheon' + tag];
261
+ // The shapes valueFor offers, in its order, so a rule they already passed
262
+ // still gets the value it always got.
263
+ const words = [tag ? tag + ' kryptheon test' : 'kryptheon test', 'kryptheon' + tag + '@example.com', 'KN' + (tag || '1'),
264
+ String(step + 1), 'https://example.com/kryptheon', 'kryptheon' + tag, 'Kryptheon' + tag, 'kn-' + (tag || '1')];
265
+ const literals = [];
266
+ for (const definition of definitions) {
267
+ const quoted = /'((?:[^']|'')*)'/g;
268
+ let one;
269
+ while ((one = quoted.exec(definition)) !== null) literals.push(one[1].split("''").join("'"));
270
+ }
271
+ const codes = ['US', 'IN', 'USD', 'INR', 'EUR', 'en', 'en-US', '#00000' + d, '2026-01-0' + (1 + (step % 9)), '1234' + d, '56000' + d,
272
+ 'kryptheon test value long enough for a rule on a minimum length ' + (tag || '0')];
273
+ const name = String(column.name).toLowerCase();
274
+ const first = /phone|mobile|whatsapp|tel|msisdn/.test(name) ? phones
275
+ : /mail/.test(name) ? emails
276
+ : /url|link|website|site|href/.test(name) ? links : [];
277
+ for (const value of first.concat(words, literals, phones, emails, links, codes)) add(fitTo(value, kind));
278
+ return out;
279
+ }
280
+
281
+ /**
282
+ * The first values a column's CHECK constraints accept, asked of the
283
+ * database itself: each rule is run on the value alone, the column given as
284
+ * the value by a one-row subquery. A rule that also names another column
285
+ * cannot be run alone and is left out; when no rule can be, nothing is
286
+ * offered and the seeder goes on as before. Remembered per table, column and
287
+ * row, so the attacks that insert later ask nothing again.
288
+ *
289
+ * Returns the accepted values, best first - possibly none.
290
+ */
291
+ const FITTED = new WeakMap();
292
+ async function fittingValues(client, table, column, distinct) {
293
+ const definitions = checksOn(table, column.name);
294
+ if (!definitions.length) return [];
295
+ const candidates = checkCandidates(column, definitions, distinct);
296
+ if (!candidates.length) return [];
297
+ let byTable = FITTED.get(table);
298
+ if (!byTable) FITTED.set(table, (byTable = new Map()));
299
+ const key = column.name + '\u0000' + String(distinct);
300
+ if (byTable.has(key)) return byTable.get(key);
301
+
302
+ const type = String(column.type);
303
+ const alone = (expression, value) =>
304
+ 'SELECT (' + expression + ') IS NOT FALSE AS ok FROM (SELECT ' + value + '::' + type + ' AS ' + quote(column.name) + ') AS kn_row';
305
+ const runnable = [];
306
+ for (const expression of definitions) {
307
+ try {
308
+ await client.query(alone(expression, 'NULL'));
309
+ runnable.push(expression);
310
+ } catch (err) {
311
+ // Names another column, or something only the table can supply.
312
+ }
313
+ }
314
+ let accepted = [];
315
+ if (runnable.length) {
316
+ const all = runnable.map((e) => '(' + e + ') IS NOT FALSE').join(' AND ');
317
+ for (const value of candidates) {
318
+ try {
319
+ const { rows } = await client.query(
320
+ 'SELECT ' + all + ' AS ok FROM (SELECT $1::' + type + ' AS ' + quote(column.name) + ') AS kn_row', [value]);
321
+ if (rows[0] && rows[0].ok === true) accepted.push(value);
322
+ // As many as the seeder will ever try; asking further changes nothing.
323
+ if (accepted.length >= SHAPES_TO_TRY) break;
324
+ } catch (err) {
325
+ // A value its own type, or the rule, refuses outright.
326
+ }
327
+ }
328
+ }
329
+ byTable.set(key, accepted);
330
+ return accepted;
331
+ }
332
+
207
333
  /**
208
334
  * Sets of columns where exactly one may be filled in.
209
335
  *
@@ -500,11 +626,14 @@ async function rowFor(client, schema, table, person, distinct, overrides, attemp
500
626
  if (column.default_expr) continue;
501
627
  if (!column.not_null && !mustFill) continue;
502
628
  columns.push(column.name);
503
- // A CHECK that lists what it will accept beats anything invented here.
629
+ // A CHECK that lists what it will accept beats anything invented here;
630
+ // after that, a value one it could not read was asked about and accepts.
504
631
  const allowed = allowedByCheck(table, column.name);
632
+ const fitting = allowed.length ? [] : await fittingValues(client, table, column, distinct);
633
+ const pick = (list) => list[Math.min(Math.max(attempt || 0, 0), list.length - 1)];
505
634
  values.push(
506
- allowed.length
507
- ? allowed[Math.min(Math.max(attempt || 0, 0), allowed.length - 1)]
635
+ allowed.length ? pick(allowed)
636
+ : fitting.length ? pick(fitting)
508
637
  : valueFor(column, person, distinct, attempt),
509
638
  );
510
639
  }
@@ -882,6 +1011,9 @@ module.exports = {
882
1011
  dependencyOrder: dependencyOrder,
883
1012
  valueFor: valueFor,
884
1013
  allowedByCheck: allowedByCheck,
1014
+ checksOn: checksOn,
1015
+ checkCandidates: checkCandidates,
1016
+ fittingValues: fittingValues,
885
1017
  exactlyOne: exactlyOne,
886
1018
  rowFor: rowFor,
887
1019
  existingRow: existingRow,
package/collision.js CHANGED
@@ -214,7 +214,12 @@ async function collide(client, one, two, schema, tables, indexes) {
214
214
 
215
215
  for (const target of candidates(tables, indexes)) {
216
216
  const table = byName.get(target.table);
217
- const value = collidingValue(target);
217
+ // A column with a CHECK of its own - an email that has to look like one -
218
+ // refused the made-up value, and the race never started. A value the
219
+ // rule was asked about and accepts is raced instead.
220
+ const column = table && (table.columns || []).find((c) => c.name === target.column);
221
+ const fitting = column ? await attack.fittingValues(client, table, column, 'collision') : [];
222
+ const value = fitting.length ? fitting[0] : collidingValue(target);
218
223
  const override = {};
219
224
  override[target.column] = value;
220
225
 
package/engine.sql CHANGED
@@ -1464,6 +1464,155 @@ BEGIN
1464
1464
  RETURN found;
1465
1465
  END $$;
1466
1466
 
1467
+ /*
1468
+ * The CHECK constraints on a table that name this column, as the expression
1469
+ * inside CHECK (...). A NOT VALID one still applies to every new row.
1470
+ * Mirrors checksOn in attack.js.
1471
+ */
1472
+ CREATE OR REPLACE FUNCTION __KN__.checks_on(tab jsonb, column_name text)
1473
+ RETURNS text[] LANGUAGE plpgsql IMMUTABLE AS $$
1474
+ DECLARE
1475
+ escaped text := regexp_replace(column_name, '([.*+?^${}()|\[\]\\])', '\\\1', 'g');
1476
+ named text;
1477
+ found text[] := ARRAY[]::text[];
1478
+ con jsonb;
1479
+ definition text;
1480
+ BEGIN
1481
+ named := '(^|[^A-Za-z0-9_$"])("' || replace(escaped, '"', '""') || '"|' || escaped || ')($|[^A-Za-z0-9_$"])';
1482
+ FOR con IN SELECT * FROM jsonb_array_elements(coalesce(tab->'constraints', '[]'::jsonb)) LOOP
1483
+ CONTINUE WHEN con->>'kind' <> 'c';
1484
+ definition := coalesce(con->>'definition', '');
1485
+ CONTINUE WHEN definition !~ named;
1486
+ found := found || regexp_replace(regexp_replace(definition, '^CHECK\s*', '', 'i'), '\s+NOT VALID\s*$', '', 'i');
1487
+ END LOOP;
1488
+ RETURN found;
1489
+ END $$;
1490
+
1491
+ /*
1492
+ * Values to offer a column whose CHECK could not be read, best first.
1493
+ *
1494
+ * Found on a real app (a clinic's, 2026-10-11): phone number columns with a
1495
+ * CHECK on their format, and a type column with a CHECK nothing here read -
1496
+ * three tables, and a fourth that pointed at one of them, never tested.
1497
+ * Nothing here reads the rule: each value is put to the rule itself
1498
+ * (fitting_values), and the first it accepts is used.
1499
+ *
1500
+ * Mirrors checkCandidates in attack.js, value for value and in the same
1501
+ * order: both engines must seed the same rows.
1502
+ */
1503
+ CREATE OR REPLACE FUNCTION __KN__.check_candidates(col jsonb, definitions text[], distinct_ text)
1504
+ RETURNS text[] LANGUAGE plpgsql IMMUTABLE AS $$
1505
+ DECLARE
1506
+ kind text := lower(coalesce(col->>'base_type', col->>'type'));
1507
+ tag text := coalesce(distinct_, '');
1508
+ step integer := CASE WHEN coalesce(distinct_, '') ~ '^[0-9]+$' THEN distinct_::integer ELSE 0 END;
1509
+ d text;
1510
+ name_ text := lower(col->>'name');
1511
+ out_ text[] := ARRAY[]::text[];
1512
+ phones text[];
1513
+ emails text[];
1514
+ links text[];
1515
+ words text[];
1516
+ literals text[] := ARRAY[]::text[];
1517
+ codes text[];
1518
+ first_ text[] := ARRAY[]::text[];
1519
+ definition text;
1520
+ one text;
1521
+ value_ text;
1522
+ BEGIN
1523
+ IF kind ~ '^(integer|bigint|smallint|numeric|decimal|real|double)' THEN
1524
+ FOREACH value_ IN ARRAY ARRAY[(1 + step)::text, (18 + step)::text, (100 + step)::text, (1000 + step)::text, '0', '5'] LOOP
1525
+ IF NOT (value_ = ANY (out_)) THEN out_ := out_ || value_; END IF;
1526
+ END LOOP;
1527
+ FOREACH definition IN ARRAY coalesce(definitions, ARRAY[]::text[]) LOOP
1528
+ FOR one IN SELECT (m)[1] FROM regexp_matches(definition, '(-?\d+(?:\.\d+)?)', 'g') m LOOP
1529
+ IF NOT (one = ANY (out_)) THEN out_ := out_ || one; END IF;
1530
+ END LOOP;
1531
+ END LOOP;
1532
+ RETURN out_;
1533
+ END IF;
1534
+ IF kind !~ '^(text|character|varchar|citext)' THEN RETURN out_; END IF;
1535
+ d := (step % 10)::text;
1536
+ phones := ARRAY['+1415555012' || d, '+919876543' || (210 + step)::text, '9876543' || (210 + step)::text, '415555012' || d,
1537
+ '+1 415-555-012' || d, '(415) 555-012' || d, '415-555-012' || d, '+44 7700 90012' || d, '07700 90012' || d];
1538
+ emails := ARRAY['kryptheon' || tag || '@example.com'];
1539
+ links := ARRAY['https://example.com/kryptheon' || tag];
1540
+ -- The shapes value_for offers, in its order, so a rule they already passed
1541
+ -- still gets the value it always got.
1542
+ words := ARRAY[CASE WHEN tag <> '' THEN tag || ' kryptheon test' ELSE 'kryptheon test' END,
1543
+ 'kryptheon' || tag || '@example.com', 'KN' || CASE WHEN tag <> '' THEN tag ELSE '1' END,
1544
+ (step + 1)::text, 'https://example.com/kryptheon', 'kryptheon' || tag, 'Kryptheon' || tag,
1545
+ 'kn-' || CASE WHEN tag <> '' THEN tag ELSE '1' END];
1546
+ FOREACH definition IN ARRAY coalesce(definitions, ARRAY[]::text[]) LOOP
1547
+ FOR one IN SELECT (m)[1] FROM regexp_matches(definition, '''((?:[^'']|'''')*)''', 'g') m LOOP
1548
+ literals := literals || replace(one, '''''', '''');
1549
+ END LOOP;
1550
+ END LOOP;
1551
+ codes := ARRAY['US', 'IN', 'USD', 'INR', 'EUR', 'en', 'en-US', '#00000' || d, '2026-01-0' || (1 + (step % 9))::text,
1552
+ '1234' || d, '56000' || d,
1553
+ 'kryptheon test value long enough for a rule on a minimum length ' || CASE WHEN tag <> '' THEN tag ELSE '0' END];
1554
+ IF name_ ~ 'phone|mobile|whatsapp|tel|msisdn' THEN first_ := phones;
1555
+ ELSIF name_ ~ 'mail' THEN first_ := emails;
1556
+ ELSIF name_ ~ 'url|link|website|site|href' THEN first_ := links;
1557
+ END IF;
1558
+ FOREACH value_ IN ARRAY first_ || words || literals || phones || emails || links || codes LOOP
1559
+ value_ := __KN__.fit_to(value_, kind);
1560
+ IF NOT (value_ = ANY (out_)) THEN out_ := out_ || value_; END IF;
1561
+ END LOOP;
1562
+ RETURN out_;
1563
+ END $$;
1564
+
1565
+ /*
1566
+ * The first values a column's CHECK constraints accept, asked of the
1567
+ * database itself: each rule is run on the value alone, the column given as
1568
+ * the value by a one-row subquery. A rule that also names another column
1569
+ * cannot be run alone and is left out; when no rule can be, nothing is
1570
+ * offered and the seeder goes on as before. At most five - as many as the
1571
+ * seeder ever tries. Mirrors fittingValues in attack.js.
1572
+ */
1573
+ CREATE OR REPLACE FUNCTION __KN__.fitting_values(tab jsonb, col jsonb, distinct_ text)
1574
+ RETURNS text[] LANGUAGE plpgsql AS $$
1575
+ DECLARE
1576
+ name_ text := col->>'name';
1577
+ type_ text := col->>'type';
1578
+ definitions text[] := __KN__.checks_on(tab, col->>'name');
1579
+ candidates text[];
1580
+ runnable text[] := ARRAY[]::text[];
1581
+ all_ text;
1582
+ accepted text[] := ARRAY[]::text[];
1583
+ expression text;
1584
+ value_ text;
1585
+ ok boolean;
1586
+ BEGIN
1587
+ IF coalesce(array_length(definitions, 1), 0) = 0 THEN RETURN accepted; END IF;
1588
+ candidates := __KN__.check_candidates(col, definitions, distinct_);
1589
+ IF coalesce(array_length(candidates, 1), 0) = 0 THEN RETURN accepted; END IF;
1590
+ FOREACH expression IN ARRAY definitions LOOP
1591
+ BEGIN
1592
+ EXECUTE format('SELECT (%s) IS NOT FALSE AS ok FROM (SELECT NULL::%s AS %I) AS kn_row', expression, type_, name_);
1593
+ runnable := runnable || expression;
1594
+ EXCEPTION WHEN OTHERS THEN
1595
+ -- Names another column, or something only the table can supply.
1596
+ NULL;
1597
+ END;
1598
+ END LOOP;
1599
+ IF coalesce(array_length(runnable, 1), 0) = 0 THEN RETURN accepted; END IF;
1600
+ SELECT string_agg('(' || e || ') IS NOT FALSE', ' AND ') INTO all_ FROM unnest(runnable) e;
1601
+ FOREACH value_ IN ARRAY candidates LOOP
1602
+ BEGIN
1603
+ EXECUTE format('SELECT %s AS ok FROM (SELECT $1::%s AS %I) AS kn_row', all_, type_, name_) INTO ok USING value_;
1604
+ IF ok IS TRUE THEN
1605
+ accepted := accepted || value_;
1606
+ EXIT WHEN array_length(accepted, 1) >= 5;
1607
+ END IF;
1608
+ EXCEPTION WHEN OTHERS THEN
1609
+ -- A value its own type, or the rule, refuses outright.
1610
+ NULL;
1611
+ END;
1612
+ END LOOP;
1613
+ RETURN accepted;
1614
+ END $$;
1615
+
1467
1616
  /*
1468
1617
  * Something valid to put in a column, so a row can exist at all.
1469
1618
  *
@@ -1983,8 +2132,12 @@ BEGIN
1983
2132
  CONTINUE WHEN NOT (col->>'not_null')::boolean AND NOT must_fill;
1984
2133
 
1985
2134
  columns := columns || to_jsonb(name);
1986
- -- A CHECK that lists what it will accept beats anything invented here.
2135
+ -- A CHECK that lists what it will accept beats anything invented here;
2136
+ -- after that, a value one it could not read was asked about and accepts.
1987
2137
  allowed := __KN__.allowed_by_check(tab, name);
2138
+ IF coalesce(array_length(allowed, 1), 0) = 0 THEN
2139
+ allowed := __KN__.fitting_values(tab, col, distinct_);
2140
+ END IF;
1988
2141
  IF array_length(allowed, 1) > 0 THEN
1989
2142
  nth := least(greatest(coalesce(attempt, 0), 0), array_length(allowed, 1) - 1);
1990
2143
  values_ := values_ || jsonb_build_array(to_jsonb(allowed[nth + 1]));
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "kryptheon-night",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Attacks a copy of your Supabase database and tells you in plain English what got in.",
5
- "license": "MIT",
5
+ "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",
7
7
  "bin": {
8
8
  "kryptheon-night": "bin/kryptheon-night.js"
@@ -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 && node installscript.check.js && node tenants.check.js && node takeover.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 && node tenants.check.js && node takeover.check.js && node checkfit.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",
@@ -77,7 +77,8 @@
77
77
  "check:buckets": "node buckets.check.js",
78
78
  "check:installscript": "node installscript.check.js",
79
79
  "check:tenants": "node tenants.check.js",
80
- "check:takeover": "node takeover.check.js"
80
+ "check:takeover": "node takeover.check.js",
81
+ "check:checkfit": "node checkfit.check.js"
81
82
  },
82
83
  "keywords": [
83
84
  "supabase",
@@ -88,11 +89,8 @@
88
89
  "lovable",
89
90
  "cli"
90
91
  ],
91
- "repository": {
92
- "type": "git",
93
- "url": "git+https://github.com/jhashantanu987-beep/kryptheon-night.git"
94
- },
95
92
  "dependencies": {
96
93
  "pg": "^8.23.0"
97
- }
94
+ },
95
+ "homepage": "https://kryptheon.tech"
98
96
  }