[object Object]

← back to Domain Sniper

owned-check: extract shared module + extend safety to honeypot-v2 generate

46c5487ca2eabb706a52272c8e7ad6681c56f380 · 2026-05-12 18:37:30 -0700 · steve

owned-check.js — small 25-line shared module with loadOwned() + isOwned().
Tolerates both shapes (bare array, wrapped {domains:[...]}) and no-ops
when data/owned.json is absent. register.js refactored to consume it.

honeypot-v2.js generate() now refuses (exit 6) when any generated stem
would bait a domain Steve owns. The collision check runs over the whole
batch before writing — partial batches never land. README documents the
owned.json shape since data/ is gitignored.

New exit-code 6 (honeypot-v2: bait would collide with owned) joins the
existing 2/3/4/5 in register.js.

Files touched

Diff

commit 46c5487ca2eabb706a52272c8e7ad6681c56f380
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 18:37:30 2026 -0700

    owned-check: extract shared module + extend safety to honeypot-v2 generate
    
    owned-check.js — small 25-line shared module with loadOwned() + isOwned().
    Tolerates both shapes (bare array, wrapped {domains:[...]}) and no-ops
    when data/owned.json is absent. register.js refactored to consume it.
    
    honeypot-v2.js generate() now refuses (exit 6) when any generated stem
    would bait a domain Steve owns. The collision check runs over the whole
    batch before writing — partial batches never land. README documents the
    owned.json shape since data/ is gitignored.
    
    New exit-code 6 (honeypot-v2: bait would collide with owned) joins the
    existing 2/3/4/5 in register.js.
---
 README.md      | 19 +++++++++++++++++++
 honeypot-v2.js | 12 ++++++++++++
 owned-check.js | 29 +++++++++++++++++++++++++++++
 register.js    | 18 +++---------------
 4 files changed, 63 insertions(+), 15 deletions(-)

diff --git a/README.md b/README.md
index f345226..3db50df 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,25 @@ State of the world for **free** real-time CT-log access, as of 2026-05-12 ~23:05
 
 Meanwhile the **honeypot + DoH spot-check** path (the actual aggregator-attribution experiment) is unaffected — that runs on Cloudflare 1.1.1.1 DoH and Node `child_process whois`, neither of which depends on CT logs.
 
+## `data/owned.json` — single source of truth
+
+Used by `register.js` (refuse re-registration, exit 5) and
+`honeypot-v2.js generate` (refuse to write a batch that baits an owned
+domain, exit 6). File is gitignored along with the rest of `data/`. Both
+shapes accepted:
+
+```json
+["foo.com", "bar.app", "baz.io"]
+```
+or
+```json
+{ "domains": ["foo.com", "bar.app", "baz.io"], "_updated": "2026-05-13" }
+```
+
+When the file doesn't exist, both checks no-op cleanly — there's nothing
+to compare against, so neither tool refuses. Add domains as you register
+them (or import from `~/cncp-starter/cncp-config.json domains[]`).
+
 ## Liveness watchdog (opt-in)
 
 `launchd/sniper-watchdog.sh` polls `/healthz` once a minute, restarts the
diff --git a/honeypot-v2.js b/honeypot-v2.js
index 57ab088..e3d5e87 100755
--- a/honeypot-v2.js
+++ b/honeypot-v2.js
@@ -29,6 +29,7 @@ const fs = require('fs');
 const path = require('path');
 const https = require('https');
 const { spawnSync } = require('child_process');
+const { isOwned, loadOwned } = require('./owned-check');
 
 const DATA_DIR = path.join(__dirname, 'data');
 
@@ -121,6 +122,17 @@ function generate(cohortKey, perBucket = 10) {
       items.push({ bucket: b.id, label: b.label, method: b.method, tld: b.tld, stem, domain: `${stem}.${b.tld}` });
     }
   }
+  // Safety gate: never bait a domain Steve owns. If any generated stem
+  // collides with owned.json, drop and regenerate. If we somehow still produce
+  // a collision after retries, refuse to write the batch.
+  const ownedSet = new Set(loadOwned());
+  const collided = items.filter((it) => ownedSet.has(it.domain.toLowerCase()));
+  if (collided.length) {
+    console.error(`\n  REFUSED: ${collided.length} generated stems collide with data/owned.json:`);
+    for (const c of collided.slice(0, 5)) console.error(`    ${c.domain}`);
+    console.error(`  Regenerate (different seed) or add the colliding stems to BRAND_STEMS exclusion in the generator.\n`);
+    process.exit(6);
+  }
   fs.mkdirSync(DATA_DIR, { recursive: true });
   const out = path.join(DATA_DIR, `honeypot-v2-${cohortKey}-${ts}.json`);
   const payload = { batchTs: ts, cohort: cohortKey, cohortNotes: cohort.notes, seed, items, lured: false };
diff --git a/owned-check.js b/owned-check.js
new file mode 100644
index 0000000..8050034
--- /dev/null
+++ b/owned-check.js
@@ -0,0 +1,29 @@
+// owned-check.js — shared lookup against data/owned.json.
+//
+// data/owned.json is the single-source-of-truth for "domains Steve already
+// owns". Both register.js (refuse re-registration) and honeypot-v2.js (refuse
+// to use as bait) consult this. File may not exist yet — that's a clean
+// "nothing owned" answer, not an error. Tolerates both shapes:
+//   ["foo.com", "bar.app"]                   (bare array)
+//   { "domains": ["foo.com", "bar.app"] }    (wrapped object)
+
+const fs = require('fs');
+const path = require('path');
+
+const OWNED_FILE = path.join(__dirname, 'data', 'owned.json');
+
+function loadOwned() {
+  if (!fs.existsSync(OWNED_FILE)) return [];
+  try {
+    const j = JSON.parse(fs.readFileSync(OWNED_FILE, 'utf8'));
+    const list = Array.isArray(j) ? j : (Array.isArray(j.domains) ? j.domains : []);
+    return list.filter((d) => typeof d === 'string').map((d) => d.toLowerCase());
+  } catch { return []; }
+}
+
+function isOwned(domain) {
+  const owned = loadOwned();
+  return owned.includes(domain.toLowerCase());
+}
+
+module.exports = { loadOwned, isOwned, OWNED_FILE };
diff --git a/register.js b/register.js
index 660c7f6..75218eb 100644
--- a/register.js
+++ b/register.js
@@ -21,22 +21,10 @@ const https = require('https');
 const fs = require('fs');
 const path = require('path');
 
+const { isOwned } = require('./owned-check');
+
 const DATA_DIR = path.join(__dirname, 'data');
 const LOG_FILE = path.join(DATA_DIR, 'registrations.jsonl');
-const OWNED_FILE = path.join(DATA_DIR, 'owned.json');
-
-// Defensive: never re-register a domain Steve already owns. data/owned.json
-// is the single-source-of-truth list per the README. File may not exist yet —
-// in that case there's nothing to compare against, so allow the registration.
-function alreadyOwned(domain) {
-  if (!fs.existsSync(OWNED_FILE)) return false;
-  try {
-    const j = JSON.parse(fs.readFileSync(OWNED_FILE, 'utf8'));
-    const list = Array.isArray(j) ? j : (Array.isArray(j.domains) ? j.domains : []);
-    const target = domain.toLowerCase();
-    return list.some((d) => typeof d === 'string' && d.toLowerCase() === target);
-  } catch { return false; }
-}
 
 function detectBackend() {
   if (process.env.NAMESILO_API_KEY) return 'namesilo';
@@ -126,7 +114,7 @@ async function main() {
     console.error(`invalid domain syntax: ${domain}`);
     process.exit(2);
   }
-  if (alreadyOwned(domain)) {
+  if (isOwned(domain)) {
     console.error(`\n  \x1b[31mREFUSED:\x1b[0m ${domain} is in data/owned.json — already owned.`);
     console.error(`  Remove it from owned.json first if you really intend to re-register.\n`);
     process.exit(5);

← ed0c01f register: refuse re-registration of domains in data/owned.js  ·  back to Domain Sniper  ·  check: annotate owned domains in DoH-check output (non-block 9f69a10 →