invent): * - Map layout (grid): browse area + profile/create surface; launch CTAs after ENTER. * - Cards always show Contact: line (or honest missing). Empty: Create your card CTA. * - DNA line: DATE · personals rose. Empire entrance (chat DNA): Options · Mirrors · Donate + ENTER. * - Risk line includes 18+. No site-local NSU wallet — cashflow on trade. */ /* * FILE MAP (orientation only — do not reorder regions by this comment) * CHARTER — FINAL-PACK MAINTENANCE CHARTER (file head) * CONTRACT — NOSIGNUP.DATE CONTRACT / layout DNA (just above) * NSP PANEL — nsp_* vault/hash/admin API + nsp_render_controlpanel() * NSD STORAGE — NSD_* constants + nsd_base…nsd_mirrors_* (shards/media/rate) * NSD API — ?src=1 / ?download=1 + if (isset($_GET['api'])) handlers * CP ROUTE — $wantPanel → nsp_render_controlpanel() early exit * HTML/CSS — main shell styles (#ContainerAll map chrome) * ENTRY CSS — empire entry / polish / after-hint / designer styles (main page only) * HTML BODY — #ContainerAll map/card DOM (browse/profile/map panels) * MAP/CARD JS — main '; exit; } /* NOSIGNUP DATE - one-file ephemeral personals. No accounts, no database, no build step. The browser owns the identity key; PHP stores public profile/media bytes for 32 days and forgets them. */ define('NSD_RATE_WINDOW', 300); define('NSD_MAX_IDS_PER_IP', 16); define('NSD_BROWSE_MAX', 120); define('NSD_EXPIRY_DAYS', 32); define('NSD_EXPIRY_SEC', NSD_EXPIRY_DAYS * 86400); define('NSD_REMOTE_TILE', '_remote'); define('NSD_MAX_UPLOAD_BYTES', 2 * 1024 * 1024); function nsd_base() { static $b = null; if ($b !== null) return $b; $c = []; if (getenv('NSD_DATA_DIR')) $c[] = rtrim(getenv('NSD_DATA_DIR'), "/\\"); if (DIRECTORY_SEPARATOR === '/') $c[] = '/var/lib/nosignup/date'; $c[] = __DIR__ . DIRECTORY_SEPARATOR . '.nsd-data'; $c[] = rtrim(sys_get_temp_dir(), "/\\") . DIRECTORY_SEPARATOR . 'nsd-data'; foreach ($c as $d) { if ($d === '' || (!is_dir($d) && !@mkdir($d, 0755, true) && !is_dir($d))) continue; $h = $d . DIRECTORY_SEPARATOR . '.htaccess'; if ((file_exists($h) && is_writable($d)) || @file_put_contents($h, "Require all denied\nDeny from all\n") !== false) { $b = $d; return $b; } } $b = sys_get_temp_dir(); return $b; } function nsd_shards() { return nsd_base() . DIRECTORY_SEPARATOR . 'shards'; } function nsd_media_dir() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'media'; if (!is_dir($d)) @mkdir($d, 0755, true); return $d; } /** media/{substr(id,0,2)}/ — 256-way prefix fan-out (durable; not RAM). */ function nsd_media_pref($id) { return substr((string)$id, 0, 2); } function nsd_media_shard_dir($id, $create = true) { $d = nsd_media_dir() . DIRECTORY_SEPARATOR . nsd_media_pref($id); if ($create && !is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_logdir() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'kyc'; if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } /** kyc/{sprintf('%08x',crc32(IP))}/ day logs — dirent fan-out like chat/work; not RAM. */ function nsd_kyc_ipdir($ip = null) { $ip = ($ip === null || $ip === '') ? (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0') : (string)$ip; $d = nsd_logdir() . DIRECTORY_SEPARATOR . sprintf('%08x', crc32($ip)); if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } function nsd_ratedir() { if (DIRECTORY_SEPARATOR === '/' && is_dir('/dev/shm') && is_writable('/dev/shm')) { $d = '/dev/shm/nosignup_date_ip'; if ((is_dir($d) || @mkdir($d, 0700, true)) && is_writable($d)) return $d; } $d = nsd_base() . DIRECTORY_SEPARATOR . 'ip_posts'; if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } function nsd_json($d, $code = 200) { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); header('Access-Control-Allow-Origin: *'); header('X-NSD-Backend: file-shard'); echo json_encode($d, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); exit; } function nsd_norm_hash() { $s = (string)@file_get_contents(__FILE__); if (substr($s, 0, 3) === "\xEF\xBB\xBF") $s = substr($s, 3); $s = str_replace(["\r\n", "\r"], "\n", $s); $s = preg_replace('/[ \t]+\n/', "\n", $s); return hash('sha256', rtrim($s, "\n") . "\n"); } function nsd_san($s, $m = 2000) { $s = trim((string)($s ?? '')); $s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]+/', ' ', $s); return function_exists('mb_substr') ? mb_substr($s, 0, $m, 'UTF-8') : substr($s, 0, $m); } /** Tiny purpose brain: bio_risk - soft scam/contact-harvest score; fail open. */ function nsd_brain_bio_risk(string $name, string $bio, string $contact): array { $t = strtolower($name . ' ' . $bio . ' ' . $contact); $hits = []; $bad = [ 'whatsapp' => 0.15, 'telegram.me' => 0.2, 'onlyfans' => 0.1, 'venmo me' => 0.2, 'gift card' => 0.35, 'wire transfer' => 0.3, 'crypto giveaway' => 0.35, 'seed phrase' => 0.5, 'private key' => 0.5, 'send nudes' => 0.25, 'under 18' => 0.9, 'escort rates' => 0.2, 'cashapp' => 0.15, ]; $score = 0.0; foreach ($bad as $k => $w) { if ($k !== '' && str_contains($t, $k)) { $score += $w; $hits[] = $k; } } if ($score > 1.0) $score = 1.0; return ['brain' => 'bio_risk', 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.5]; } /** Tiny purpose brain: name_sane - display name length/token soft checks; fail open. */ function nsd_brain_name_sane(string $name): array { $n = trim($name); $notes = []; $score = 0.0; $len = function_exists('mb_strlen') ? mb_strlen($n, 'UTF-8') : strlen($n); if ($len < 1) { $notes[] = 'empty'; $score += 0.5; } if ($len > 40) { $notes[] = 'long'; $score += 0.25; } if (preg_match('/https?:\/\//i', $n) || str_contains(strtolower($n), 'www.')) { $notes[] = 'url'; $score += 0.4; } if (preg_match('/\b(seed|private\s*key|mnemonic)\b/i', $n)) { $notes[] = 'secret'; $score += 0.55; } if (preg_match('/(.)\1{6,}/u', $n)) { $notes[] = 'repeat'; $score += 0.15; } if ($score > 1.0) { $score = 1.0; } return ['brain' => 'name_sane', 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5]; } function nsd_tile_ok($t) { return is_string($t) && preg_match('/^-?\d{1,3}(?:\.5)?_-?\d{1,3}(?:\.5)?$/', $t); } function nsd_half($v) { $v = floor((float)$v * 2) / 2; return $v == (int)$v ? (string)(int)$v : (string)$v; } function nsd_tiles($tile, $r) { $p = explode('_', $tile); $lat = (float)$p[0]; $lon = (float)$p[1]; $n = 2 * max(1, min(7, (int)$r)); $out = []; for ($dy = -$n; $dy <= $n; $dy++) for ($dx = -$n; $dx <= $n; $dx++) $out[] = nsd_half($lat + $dy * .5) . '_' . nsd_half($lon + $dx * .5); return $out; } function nsd_tile_center($tile) { $p = explode('_', $tile); return [(float)$p[0] + .25, (float)$p[1] + .25]; } function nsd_uid($v) { $v = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$v); return (strlen($v) >= 8 && strlen($v) <= 96) ? $v : 'ip'; } function nsd_owner($v) { $v = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$v); return (strlen($v) >= 16 && strlen($v) <= 128) ? $v : ''; } function nsd_owner_hash($owner) { return hash('sha256', 'nsd-owner-v1:' . $owner); } function nsd_id_ok($id) { return is_string($id) && preg_match('/^\d{10}_[a-f0-9]{10,20}$/', $id); } function nsd_new_id($uid, $owner, $ip) { try { $r = bin2hex(random_bytes(8)); } catch (Throwable $e) { $r = sprintf('%08x', mt_rand()) . sprintf('%08x', mt_rand()); } return time() . '_' . substr(hash('sha256', $uid . '|' . $owner . '|' . $ip . '|' . microtime(true) . '|' . $r), 0, 14); } function nsd_mkdir($p) { if (!is_dir($p)) @mkdir($p, 0755, true); return is_dir($p); } function nsd_rate_check($ip, $type, $uid) { $dir = nsd_ratedir(); $now = time(); if (mt_rand(1, 20) === 1) foreach (glob($dir . DIRECTORY_SEPARATOR . '*.txt') ?: [] as $g) if (@filemtime($g) < $now - NSD_RATE_WINDOW) @unlink($g); $f = $dir . DIRECTORY_SEPARATOR . md5($ip) . '.txt'; $all = is_file($f) ? (json_decode((string)@file_get_contents($f), true) ?: []) : []; if (!is_array($all)) $all = []; $m = (isset($all[$type]) && is_array($all[$type])) ? $all[$type] : []; foreach ($m as $k => $ts) if (!is_int($ts) || $now - $ts >= NSD_RATE_WINDOW) unset($m[$k]); if (isset($m[$uid])) return ['wait' => max(1, ($m[$uid] + NSD_RATE_WINDOW) - $now), 'reason' => 'soon']; if (count($m) >= NSD_MAX_IDS_PER_IP) return ['wait' => max(1, (min($m) + NSD_RATE_WINDOW) - $now), 'reason' => 'busy']; $m[$uid] = $now; $all[$type] = $m; @file_put_contents($f, json_encode($all), LOCK_EX); return ['wait' => 0, 'reason' => '']; } function nsd_browse_check($ip) { $dir = nsd_ratedir(); $now = time(); $f = $dir . DIRECTORY_SEPARATOR . md5($ip) . '.txt'; $all = is_file($f) ? (json_decode((string)@file_get_contents($f), true) ?: []) : []; if (!is_array($all)) $all = []; $b = array_values(array_filter((isset($all['browse']) && is_array($all['browse'])) ? $all['browse'] : [], fn($t) => is_int($t) && $now - $t < NSD_RATE_WINDOW)); if (count($b) >= NSD_BROWSE_MAX) { sort($b); return max(1, ($b[0] + NSD_RATE_WINDOW) - $now); } $b[] = $now; $all['browse'] = $b; @file_put_contents($f, json_encode($all), LOCK_EX); return 0; } /** Count-bucket rate (like browse): max $max events of $bucket per IP per window. */ function nsd_count_rate($ip, $bucket, $max) { $dir = nsd_ratedir(); $now = time(); $f = $dir . DIRECTORY_SEPARATOR . md5($ip) . '.txt'; $all = is_file($f) ? (json_decode((string)@file_get_contents($f), true) ?: []) : []; if (!is_array($all)) $all = []; $b = array_values(array_filter((isset($all[$bucket]) && is_array($all[$bucket])) ? $all[$bucket] : [], fn($t) => is_int($t) && $now - $t < NSD_RATE_WINDOW)); if (count($b) >= $max) { sort($b); return max(1, ($b[0] + NSD_RATE_WINDOW) - $now); } $b[] = $now; $all[$bucket] = $b; @file_put_contents($f, json_encode($all), LOCK_EX); return 0; } function nsd_log($act, $loc) { $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); $dir = nsd_kyc_ipdir($ip); $now = time(); /* GC own crc32 shard only; interactions deleted after 32 days (lazy, no cron). */ if (mt_rand(1, 50) === 1) foreach (glob($dir . DIRECTORY_SEPARATOR . '*.log.txt') ?: [] as $g) if (@filemtime($g) < $now - NSD_EXPIRY_SEC) @unlink($g); $f = $dir . DIRECTORY_SEPARATOR . date('Y-m-d') . '.log.txt'; if (is_file($f) && @filesize($f) >= 51200) $f = $dir . DIRECTORY_SEPARATOR . date('Y-m-d_His') . '.log.txt'; $line = json_encode(['ts' => $now, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'xff' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '', 'ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 260), 'act' => $act, 'loc' => $loc], JSON_UNESCAPED_SLASHES) . "\n"; @file_put_contents($f, $line, FILE_APPEND | LOCK_EX); } /** Resolve media under media/{substr(id,0,2)}/{id}.* then flat back-compat media/{id}.* so pre-migration files still serve. */ function nsd_media_path($id) { if (!nsd_id_ok($id)) return null; $shard = nsd_media_dir() . DIRECTORY_SEPARATOR . nsd_media_pref($id); foreach (['jpg', 'jpeg', 'png', 'webp', 'gif'] as $e) { $p = $shard . DIRECTORY_SEPARATOR . $id . '.' . $e; if (is_file($p)) return $p; } /* back-compat: pre-migration flat layout */ foreach (['jpg', 'jpeg', 'png', 'webp', 'gif'] as $e) { $p = nsd_media_dir() . DIRECTORY_SEPARATOR . $id . '.' . $e; if (is_file($p)) return $p; } return null; } /** Unlink media for id under prefix shard AND legacy flat (delete + re-upload cleanup). */ function nsd_media_unlink_all($id) { if (!nsd_id_ok($id)) return; $shard = nsd_media_dir() . DIRECTORY_SEPARATOR . nsd_media_pref($id); if (is_dir($shard)) foreach (glob($shard . DIRECTORY_SEPARATOR . $id . '.*') ?: [] as $f) @unlink($f); foreach (glob(nsd_media_dir() . DIRECTORY_SEPARATOR . $id . '.*') ?: [] as $f) @unlink($f); } function nsd_delete_profile_files($id, $media = false) { if (!nsd_id_ok($id)) return; foreach (glob(nsd_shards() . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') ?: [] as $f) @unlink($f); if ($media) nsd_media_unlink_all($id); } function nsd_find_profile($id) { if (!nsd_id_ok($id)) return null; $files = glob(nsd_shards() . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') ?: []; usort($files, function ($a, $b) { $ar = strpos($a, DIRECTORY_SEPARATOR . NSD_REMOTE_TILE . DIRECTORY_SEPARATOR) !== false; $br = strpos($b, DIRECTORY_SEPARATOR . NSD_REMOTE_TILE . DIRECTORY_SEPARATOR) !== false; return ($ar <=> $br); }); foreach ($files as $f) { if (@filemtime($f) < time() - NSD_EXPIRY_SEC) { @unlink($f); continue; } $d = json_decode((string)@file_get_contents($f), true); if (is_array($d)) return [$f, $d]; } return null; } function nsd_put_profile($d) { $id = $d['id'] ?? ''; $tile = $d['tile'] ?? ''; if (!nsd_id_ok($id) || !nsd_tile_ok($tile)) return false; nsd_delete_profile_files($id, false); $json = json_encode($d, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); if ($json === false) return false; $p = nsd_shards() . DIRECTORY_SEPARATOR . $tile; if (!nsd_mkdir($p)) return false; if (@file_put_contents($p . DIRECTORY_SEPARATOR . $id . '.profile.txt', $json, LOCK_EX) === false) return false; if (!empty($d['remote'])) { $r = nsd_shards() . DIRECTORY_SEPARATOR . NSD_REMOTE_TILE; if (nsd_mkdir($r)) @file_put_contents($r . DIRECTORY_SEPARATOR . $id . '.profile.txt', $json, LOCK_EX); } return true; } function nsd_public_profile($d) { unset($d['owner_hash']); $id = $d['id'] ?? ''; $mp = nsd_media_path($id); if ($mp) $d['photo'] = 'index.php?api=media&id=' . rawurlencode($id) . '&v=' . (int)@filemtime($mp); $d['expires_in'] = max(0, (($d['timestamp'] ?? time()) + NSD_EXPIRY_SEC) - time()); return $d; } function nsd_list_profiles($tile, $radius, $remote) { $now = time(); $by = []; foreach (nsd_tiles($tile, $radius) as $t) { $dir = nsd_shards() . DIRECTORY_SEPARATOR . $t; if (!is_dir($dir)) continue; foreach (glob($dir . DIRECTORY_SEPARATOR . '*.profile.txt') ?: [] as $f) { if (@filemtime($f) < $now - NSD_EXPIRY_SEC) { @unlink($f); continue; } $d = json_decode((string)@file_get_contents($f), true); if (!is_array($d) || empty($d['id'])) continue; $by[$d['id']] = nsd_public_profile($d); } } if ($remote) { $dir = nsd_shards() . DIRECTORY_SEPARATOR . NSD_REMOTE_TILE; if (is_dir($dir)) foreach (glob($dir . DIRECTORY_SEPARATOR . '*.profile.txt') ?: [] as $f) { if (@filemtime($f) < $now - NSD_EXPIRY_SEC) { @unlink($f); continue; } $d = json_decode((string)@file_get_contents($f), true); if (!is_array($d) || empty($d['id'])) continue; $by[$d['id']] = nsd_public_profile($d); } } krsort($by); return array_values($by); } function nsd_mime_ext($mime) { return ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'][$mime] ?? ''; } function nsd_image_mime($file) { $h = (string)@file_get_contents($file, false, null, 0, 16); if (substr($h, 0, 8) === "\x89PNG\x0D\x0A\x1A\x0A") return 'image/png'; if (substr($h, 0, 3) === "\xFF\xD8\xFF") return 'image/jpeg'; if (substr($h, 0, 6) === 'GIF87a' || substr($h, 0, 6) === 'GIF89a') return 'image/gif'; if (substr($h, 0, 4) === 'RIFF' && substr($h, 8, 4) === 'WEBP') return 'image/webp'; return ''; } function nsd_mirrorfile() { return nsd_ratedir() . DIRECTORY_SEPARATOR . 'mirrors.txt'; } function nsd_mirror_ok($u) { $u = trim((string)$u); if ($u === '') return null; if (!preg_match('#^https?://#i', $u)) $u = 'https://' . $u; $p = parse_url($u); if (!$p || empty($p['host'])) return null; $host = strtolower($p['host']); if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.|169\.254\.|::1)/i', $host)) return null; return rtrim($u, '/'); } function nsd_mirrors_read() { $f = nsd_mirrorfile(); if (!is_file($f)) return []; $out = []; $now = time(); foreach (file($f, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $p = explode("\t", $line); $u = nsd_mirror_ok($p[0] ?? ''); $t = (int)($p[1] ?? 0); if ($u && $now - $t < NSD_EXPIRY_SEC) $out[$u] = $t; } return $out; } function nsd_mirrors_add($txt) { $cur = nsd_mirrors_read(); $now = time(); $changed = false; foreach (preg_split('/[\s,]+/', (string)$txt) ?: [] as $u) { $u = nsd_mirror_ok($u); if (!$u) continue; $cur[$u] = $now; $changed = true; } if (!$changed) return; if (count($cur) > 64) $cur = array_slice($cur, -64, null, true); $s = ''; foreach ($cur as $u => $t) $s .= $u . "\t" . $t . "\n"; @file_put_contents(nsd_mirrorfile(), $s, LOCK_EX); } function nsd_mirrors_sample($n) { $u = array_keys(nsd_mirrors_read()); if (!$u) return []; shuffle($u); return array_slice($u, 0, $n); } /* ======================================================================== * BLACK-BOOK MATCH + PAIR THREADS (DATE-MATCH) * Identity = browser-owned nsd_owner key (hashed server-side). No accounts. * Semantics (operator law — implement exactly): * MATCH A→B = record so B's black book lists A; A still cannot message B. * BLACK BOOK = people who matched ME (actionable message-gates only). * SEND msg = append pair .txt + auto-match me→them (two-way if both). * Storage lives under nsd_base() (NSD_DATA_DIR / .nsd-data), same TTL as cards. * Not a ranking engine, not a global social graph, not money. * ======================================================================== */ function nsd_matches_root() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'matches'; if (!is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_threads_root() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'threads'; if (!is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_owners_root() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'owners'; if (!is_dir($d)) @mkdir($d, 0755, true); return $d; } /** 256-way prefix from a 64-hex owner hash (or any long id). */ function nsd_hash_pref($h) { return substr((string)$h, 0, 2); } /** Durable reverse index: owner_hash → latest profile id (for black-book cards). */ function nsd_owner_index_path($owner_hash) { $pref = nsd_hash_pref($owner_hash); $dir = nsd_owners_root() . DIRECTORY_SEPARATOR . $pref; if (!is_dir($dir)) @mkdir($dir, 0755, true); return $dir . DIRECTORY_SEPARATOR . $owner_hash . '.owner.txt'; } function nsd_owner_index_put($owner_hash, $profile_id) { if ($owner_hash === '' || !nsd_id_ok($profile_id)) return false; $p = nsd_owner_index_path($owner_hash); return @file_put_contents($p, $profile_id . "\n" . (string)time(), LOCK_EX) !== false; } function nsd_owner_index_get($owner_hash) { if ($owner_hash === '' || !preg_match('/^[a-f0-9]{64}$/', $owner_hash)) return null; $p = nsd_owner_index_path($owner_hash); if (!is_file($p)) return null; if (@filemtime($p) < time() - NSD_EXPIRY_SEC) { @unlink($p); return null; } $lines = file($p, FILE_IGNORE_NEW_LINES); $id = trim((string)($lines[0] ?? '')); return nsd_id_ok($id) ? $id : null; } function nsd_owner_index_clear($owner_hash) { if ($owner_hash === '') return; $p = nsd_owner_index_path($owner_hash); if (is_file($p)) @unlink($p); } /** Directory of match records targeting $to_owner_hash (their black book). */ function nsd_match_to_dir($to_owner_hash, $create = true) { $pref = nsd_hash_pref($to_owner_hash); $d = nsd_matches_root() . DIRECTORY_SEPARATOR . $pref . DIRECTORY_SEPARATOR . $to_owner_hash; if ($create && !is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_match_file($to_owner_hash, $from_owner_hash) { return nsd_match_to_dir($to_owner_hash, true) . DIRECTORY_SEPARATOR . $from_owner_hash . '.match.txt'; } /** * Record match FROM $from_owner (raw key) TO profile $to_profile_id. * Stored under the TARGET's owner_hash so it appears in THEIR black book. * Returns ['ok'=>true, 'record'=>...] or ['error'=>..., 'code'=>...]. */ function nsd_record_match_core($from_owner, $to_profile_id) { $from_owner = nsd_owner($from_owner); if ($from_owner === '') return ['error' => 'missing browser owner key', 'code' => 400]; if (!nsd_id_ok($to_profile_id)) return ['error' => 'bad target profile', 'code' => 400]; $found = nsd_find_profile($to_profile_id); if (!$found) return ['error' => 'target profile not found', 'code' => 404]; $to_data = $found[1]; $to_oh = (string)($to_data['owner_hash'] ?? ''); if ($to_oh === '' || !preg_match('/^[a-f0-9]{64}$/', $to_oh)) return ['error' => 'target has no owner', 'code' => 400]; $from_oh = nsd_owner_hash($from_owner); if ($from_oh === $to_oh) return ['error' => 'cannot match yourself', 'code' => 400]; /* Prefer caller's published profile id for the black-book card. */ $from_pid = nsd_owner_index_get($from_oh); if ($from_pid === null) { /* Fallback: scan is avoided — require a public card to match others. */ return ['error' => 'publish your profile before matching', 'code' => 400]; } $rec = [ 'from_owner_hash' => $from_oh, 'from_profile_id' => $from_pid, 'to_owner_hash' => $to_oh, 'to_profile_id' => $to_profile_id, 'ts' => time(), ]; $json = json_encode($rec, JSON_UNESCAPED_SLASHES); if ($json === false) return ['error' => 'encode fail', 'code' => 500]; if (@file_put_contents(nsd_match_file($to_oh, $from_oh), $json, LOCK_EX) === false) { return ['error' => 'storage not writable', 'code' => 500]; } return ['ok' => true, 'record' => $rec]; } /** True if $from_oh has a live match record targeting $to_oh. */ function nsd_has_match($from_oh, $to_oh) { if ($from_oh === '' || $to_oh === '') return false; $f = nsd_match_to_dir($to_oh, false) . DIRECTORY_SEPARATOR . $from_oh . '.match.txt'; if (!is_file($f)) return false; if (@filemtime($f) < time() - NSD_EXPIRY_SEC) { @unlink($f); return false; } return true; } /** * Black book for $owner (raw key) = people who matched ME. * Returns public profile cards + peer_profile_id for messaging. */ function nsd_blackbook_list($owner) { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'missing browser owner key', 'code' => 400]; $me = nsd_owner_hash($owner); $dir = nsd_match_to_dir($me, false); $entries = []; if (is_dir($dir)) { $now = time(); foreach (glob($dir . DIRECTORY_SEPARATOR . '*.match.txt') ?: [] as $f) { if (@filemtime($f) < $now - NSD_EXPIRY_SEC) { @unlink($f); continue; } $rec = json_decode((string)@file_get_contents($f), true); if (!is_array($rec)) continue; $from_oh = (string)($rec['from_owner_hash'] ?? ''); $from_pid = (string)($rec['from_profile_id'] ?? ''); if ($from_oh === '') continue; /* Prefer live card via owner index; fall back to recorded profile id. */ $live_id = nsd_owner_index_get($from_oh); $pid = $live_id ?: $from_pid; $profile = null; if (nsd_id_ok($pid)) { $pf = nsd_find_profile($pid); if ($pf) $profile = nsd_public_profile($pf[1]); } $entries[] = [ 'peer_profile_id' => nsd_id_ok($pid) ? $pid : $from_pid, 'peer_owner_hash' => $from_oh, /* server-side only identity for threads; client uses peer_profile_id */ 'matched_at' => (int)($rec['ts'] ?? @filemtime($f)), 'i_matched_them' => nsd_has_match($me, $from_oh), 'profile' => $profile, ]; } } /* Newest first */ usort($entries, function ($a, $b) { return ($b['matched_at'] ?? 0) <=> ($a['matched_at'] ?? 0); }); /* Strip peer_owner_hash from public JSON — client only needs peer_profile_id */ $public = []; foreach ($entries as $e) { $public[] = [ 'peer_profile_id' => $e['peer_profile_id'], 'matched_at' => $e['matched_at'], 'i_matched_them' => !empty($e['i_matched_them']), 'two_way' => !empty($e['i_matched_them']), 'profile' => $e['profile'], ]; } return ['ok' => true, 'entries' => $public, 'count' => count($public)]; } /** Stable pair key: sorted owner hashes joined (order-independent thread file). */ function nsd_pair_key($oh1, $oh2) { $a = [(string)$oh1, (string)$oh2]; sort($a, SORT_STRING); return $a[0] . '__' . $a[1]; } function nsd_thread_path($oh1, $oh2) { $key = nsd_pair_key($oh1, $oh2); $pref = nsd_hash_pref($key); $dir = nsd_threads_root() . DIRECTORY_SEPARATOR . $pref; if (!is_dir($dir)) @mkdir($dir, 0755, true); return $dir . DIRECTORY_SEPARATOR . $key . '.txt'; } /** * Resolve peer owner_hash from my black book (they must have matched me). * Returns from_oh or null. */ function nsd_peer_from_blackbook($my_oh, $peer_profile_id) { if (!nsd_id_ok($peer_profile_id)) return null; $dir = nsd_match_to_dir($my_oh, false); if (!is_dir($dir)) return null; $now = time(); foreach (glob($dir . DIRECTORY_SEPARATOR . '*.match.txt') ?: [] as $f) { if (@filemtime($f) < $now - NSD_EXPIRY_SEC) { @unlink($f); continue; } $rec = json_decode((string)@file_get_contents($f), true); if (!is_array($rec)) continue; $from_oh = (string)($rec['from_owner_hash'] ?? ''); $from_pid = (string)($rec['from_profile_id'] ?? ''); $live = $from_oh !== '' ? nsd_owner_index_get($from_oh) : null; $pid = $live ?: $from_pid; if ($pid === $peer_profile_id || $from_pid === $peer_profile_id) { return $from_oh !== '' ? $from_oh : null; } } return null; } /** Read pair thread messages (all lines). Auth: peer must be in caller's black book. */ function nsd_thread_read($owner, $peer_profile_id) { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'missing browser owner key', 'code' => 400]; $me = nsd_owner_hash($owner); $peer_oh = nsd_peer_from_blackbook($me, $peer_profile_id); if ($peer_oh === null) return ['error' => 'not in your black book — they must match you first', 'code' => 403]; $path = nsd_thread_path($me, $peer_oh); $messages = []; if (is_file($path)) { if (@filemtime($path) < time() - NSD_EXPIRY_SEC) { @unlink($path); } else { foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) { if ($line === '') continue; $p = explode("\t", $line, 3); if (count($p) < 3) continue; $from = (string)$p[1]; $messages[] = [ 'ts' => (int)$p[0], 'from_me' => ($from === $me), 'text' => $p[2], ]; } } } return [ 'ok' => true, 'peer_profile_id' => $peer_profile_id, 'two_way' => nsd_has_match($me, $peer_oh), 'messages' => $messages, ]; } /** * Append a message to the pair thread. * Auth: peer must be in my black book (they matched me). * Side effect: AUTO-MATCH me → them (so reply opens two-way). */ function nsd_thread_append_core($owner, $peer_profile_id, $text) { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'missing browser owner key', 'code' => 400]; $text = nsd_san($text, 500); if ($text === '') return ['error' => 'empty message', 'code' => 400]; $me = nsd_owner_hash($owner); $peer_oh = nsd_peer_from_blackbook($me, $peer_profile_id); if ($peer_oh === null) return ['error' => 'not in your black book — they must match you first', 'code' => 403]; /* AUTO-MATCH me → peer (records so THEY can message me / two-way). */ $my_pid = nsd_owner_index_get($me); if ($my_pid === null) return ['error' => 'publish your profile before messaging', 'code' => 400]; /* Write match file targeting peer (so I appear in their black book). */ $rec = [ 'from_owner_hash' => $me, 'from_profile_id' => $my_pid, 'to_owner_hash' => $peer_oh, 'to_profile_id' => $peer_profile_id, 'ts' => time(), 'via' => 'thread_send', ]; $json = json_encode($rec, JSON_UNESCAPED_SLASHES); if ($json !== false) @file_put_contents(nsd_match_file($peer_oh, $me), $json, LOCK_EX); $path = nsd_thread_path($me, $peer_oh); $line = time() . "\t" . $me . "\t" . str_replace(["\t", "\r", "\n"], [' ', ' ', ' '], $text) . "\n"; if (@file_put_contents($path, $line, FILE_APPEND | LOCK_EX) === false) { return ['error' => 'storage not writable', 'code' => 500]; } return nsd_thread_read($owner, $peer_profile_id); } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: Content-Type'); http_response_code(204); exit; } if (isset($_GET['src']) || isset($_GET['download'])) { nsd_log(isset($_GET['download']) ? 'download' : 'src', 'index.php'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: X-NSD-Parity-SHA256, X-NSD-Source-SHA256'); header('Content-Type: text/plain; charset=utf-8'); header('Content-Disposition: ' . (isset($_GET['download']) ? 'attachment' : 'inline') . '; filename="index.php"'); header('X-NSD-Source-SHA256: ' . hash_file('sha256', __FILE__)); header('X-NSD-Parity-SHA256: ' . nsd_norm_hash()); header('Content-Length: ' . filesize(__FILE__)); readfile(__FILE__); exit; } if (isset($_GET['api'])) { $api = (string)$_GET['api']; $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; /* WIRING-PARITY: public selfhash (trade-shape; no auth) */ if ($api === 'selfhash') { nsd_json(['ok' => true, 'version' => 'date', 'sha256' => @hash_file('sha256', __FILE__) ?: null, 'file' => basename(__FILE__)]); } if (str_starts_with((string)$api, 'admin_')) { nsp_handle_admin_api((string)$api); } if ($api === 'profiles') { $tile = $_GET['tile'] ?? ''; if (!nsd_tile_ok($tile)) nsd_json(['error' => 'bad tile'], 400); $wait = nsd_browse_check($ip); if ($wait > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy'], 429); $radius = max(1, min(7, (int)($_GET['radius'] ?? 1))); $remote = !empty($_GET['remote']) && $_GET['remote'] !== '0' && $_GET['remote'] !== 'false'; nsd_log('browse_profiles', 'tile=' . $tile . ' r=' . $radius . ' remote=' . ($remote ? '1' : '0')); nsd_json(['ok' => true, 'expiry_days' => NSD_EXPIRY_DAYS, 'profiles' => nsd_list_profiles($tile, $radius, $remote), 'mirrors' => nsd_mirrors_sample(6)]); } if ($api === 'post_profile') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); if ($owner === '') nsd_json(['error' => 'missing browser owner key'], 400); $tile = $_POST['tile'] ?? ''; if (!nsd_tile_ok($tile)) nsd_json(['error' => 'bad tile'], 400); $name = nsd_san($_POST['name'] ?? '', 32); $age = (int)($_POST['age'] ?? 0); $bio = nsd_san($_POST['bio'] ?? '', 420); $contact = nsd_san($_POST['contact'] ?? '', 120); if ($name === '' || $age < 18 || $age > 120 || $contact === '') nsd_json(['error' => 'name, 18+ age, and contact required — contact on the profile is how people reach you (no account)'], 400); $bioRisk = nsd_brain_bio_risk($name, $bio, $contact); $nameSane = nsd_brain_name_sane($name); // hard reject only extreme cases (e.g. minors); otherwise fail open with soft flag if (($bioRisk['score'] ?? 0) >= 0.85) { nsd_json(['error' => 'profile rejected by bio_risk brain', 'brains' => ['bio_risk' => $bioRisk, 'name_sane' => $nameSane]], 400); } if (($nameSane['score'] ?? 0) >= 0.9) { nsd_json(['error' => 'name rejected by name_sane brain', 'brains' => ['bio_risk' => $bioRisk, 'name_sane' => $nameSane]], 400); } $id = $_POST['id'] ?? ''; $existing = nsd_id_ok($id) ? nsd_find_profile($id) : null; if ($existing) { if (($existing[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json(['error' => 'locked to another browser'], 403); } else { $rl = nsd_rate_check($ip, 'profile', $uid); if ($rl['wait'] > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $rl['wait'], 'reason' => $rl['reason']], 429); $id = nsd_new_id($uid, $owner, $ip); } $center = nsd_tile_center($tile); $lat = is_numeric($_POST['lat'] ?? null) ? max(-90, min(90, (float)$_POST['lat'])) : $center[0]; $lon = is_numeric($_POST['lon'] ?? null) ? max(-180, min(180, (float)$_POST['lon'])) : $center[1]; $looking = nsd_san($_POST['looking'] ?? 'dates', 40); $remote = !empty($_POST['remote']) && $_POST['remote'] !== '0' && $_POST['remote'] !== 'false'; $palette = ['#ff4f88', '#f5c84c', '#62d2ff', '#8de06f', '#c89bff', '#ff9a62', '#58e0c2', '#f06bff']; $color = $palette[hexdec(substr(hash('sha1', $id), 0, 2)) % count($palette)]; $data = [ 'id' => $id, 'tile' => $tile, 'name' => $name, 'age' => $age, 'looking' => $looking, 'bio' => $bio, 'contact' => $contact, 'city' => number_format($lat, 2, '.', '') . ', ' . number_format($lon, 2, '.', ''), 'note' => trim($looking . ($bio !== '' ? ' - ' . $bio : '')), 'lat' => $lat, 'lon' => $lon, 'remote' => $remote, 'color' => $color, 'timestamp' => time(), 'owner_hash' => nsd_owner_hash($owner) ]; if (!empty($bioRisk['flag'])) $data['bio_risk'] = $bioRisk; if (!empty($nameSane['flag'])) $data['name_sane'] = $nameSane; if (!nsd_put_profile($data)) nsd_json(['error' => 'storage not writable'], 500); /* Owner index: reverse map for black-book card resolution (no accounts). */ nsd_owner_index_put(nsd_owner_hash($owner), $id); nsd_log($existing ? 'update_profile' : 'post_profile', 'shards/' . $tile . '/' . $id . '.profile.txt'); nsd_json(['ok' => true, 'id' => $id, 'profile' => nsd_public_profile($data), 'expiry_days' => NSD_EXPIRY_DAYS, 'brains' => ['bio_risk' => $bioRisk, 'name_sane' => $nameSane]]); } if ($api === 'delete_profile') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['error' => 'POST only'], 405); $id = $_POST['id'] ?? ''; $owner = nsd_owner($_POST['owner'] ?? ''); $found = nsd_find_profile($id); if (!$found) nsd_json(['ok' => true, 'gone' => true]); if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json(['error' => 'locked to another browser'], 403); nsd_delete_profile_files($id, true); nsd_owner_index_clear(nsd_owner_hash($owner)); nsd_log('delete_profile', $id); nsd_json(['ok' => true]); } /* ---- DATE-MATCH APIs: match gate + black book + pair threads ---- */ if ($api === 'record_match') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); $target = (string)($_POST['target_id'] ?? $_POST['id'] ?? ''); if ($owner === '') nsd_json(['error' => 'missing browser owner key'], 400); /* Soft IP bucket: up to 40 matches / 5 min (not one-global-uid lock). */ $wait = nsd_count_rate($ip, 'match', 40); if ($wait > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy'], 429); unset($uid); /* uid accepted for client symmetry; rate is IP count-bucket */ $r = nsd_record_match_core($owner, $target); if (empty($r['ok'])) nsd_json(['error' => $r['error'] ?? 'match failed'], (int)($r['code'] ?? 400)); nsd_log('record_match', 'to=' . $target); nsd_json(['ok' => true, 'matched' => true, 'target_id' => $target, 'note' => 'They can message you now. You cannot message them until they match you.']); } if ($api === 'blackbook' || $api === 'list_matched_me') { /* GET or POST with owner — lists only people who matched ME. */ $owner = nsd_owner($_POST['owner'] ?? $_GET['owner'] ?? ''); if ($owner === '') nsd_json(['error' => 'missing browser owner key'], 400); $wait = nsd_browse_check($ip); if ($wait > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy'], 429); $r = nsd_blackbook_list($owner); if (empty($r['ok'])) nsd_json(['error' => $r['error'] ?? 'blackbook failed'], (int)($r['code'] ?? 400)); nsd_log('blackbook', 'n=' . (int)($r['count'] ?? 0)); nsd_json($r); } if ($api === 'thread_get') { $owner = nsd_owner($_POST['owner'] ?? $_GET['owner'] ?? ''); $peer = (string)($_POST['peer_id'] ?? $_GET['peer_id'] ?? ''); if ($owner === '') nsd_json(['error' => 'missing browser owner key'], 400); $r = nsd_thread_read($owner, $peer); if (empty($r['ok'])) nsd_json(['error' => $r['error'] ?? 'thread failed'], (int)($r['code'] ?? 400)); nsd_json($r); } if ($api === 'thread_append') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); $peer = (string)($_POST['peer_id'] ?? ''); $text = (string)($_POST['text'] ?? $_POST['message'] ?? ''); if ($owner === '') nsd_json(['error' => 'missing browser owner key'], 400); /* Soft IP bucket: up to 60 messages / 5 min. */ $wait = nsd_count_rate($ip, 'msg', 60); if ($wait > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy'], 429); unset($uid); $r = nsd_thread_append_core($owner, $peer, $text); if (empty($r['ok'])) nsd_json(['error' => $r['error'] ?? 'send failed'], (int)($r['code'] ?? 400)); nsd_log('thread_append', 'peer=' . $peer); nsd_json($r); } if ($api === 'upload_photo') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $id = $_POST['id'] ?? ''; $owner = nsd_owner($_POST['owner'] ?? ''); $found = nsd_find_profile($id); if (!$found) nsd_json(['error' => 'profile not found'], 404); if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json(['error' => 'locked to another browser'], 403); $rl = nsd_rate_check($ip, 'upload', $uid); if ($rl['wait'] > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $rl['wait'], 'reason' => $rl['reason']], 429); if (empty($_FILES['photo']) || !is_array($_FILES['photo']) || ($_FILES['photo']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) nsd_json(['error' => 'missing photo'], 400); if ((int)($_FILES['photo']['size'] ?? 0) <= 0 || (int)$_FILES['photo']['size'] > NSD_MAX_UPLOAD_BYTES) nsd_json(['error' => 'photo too large'], 413); $tmp = $_FILES['photo']['tmp_name']; $mime = ''; if (function_exists('finfo_open')) { $fi = finfo_open(FILEINFO_MIME_TYPE); if ($fi) { $mime = (string)finfo_file($fi, $tmp); finfo_close($fi); } } if ($mime === '' && function_exists('mime_content_type')) $mime = (string)mime_content_type($tmp); if ($mime === '') $mime = nsd_image_mime($tmp); $ext = nsd_mime_ext($mime); if ($ext === '') nsd_json(['error' => 'unsupported image type'], 415); nsd_media_unlink_all($id); $dst = nsd_media_shard_dir($id) . DIRECTORY_SEPARATOR . $id . '.' . $ext; $ok = is_uploaded_file($tmp) ? @move_uploaded_file($tmp, $dst) : @copy($tmp, $dst); if (!$ok) nsd_json(['error' => 'upload not writable'], 500); $data = $found[1]; $data['photo_mime'] = $mime; $data['media_updated'] = time(); nsd_put_profile($data); nsd_log('upload_photo', $id); nsd_json(['ok' => true, 'profile' => nsd_public_profile($data)]); } if ($api === 'media') { $id = $_GET['id'] ?? ''; $p = nsd_media_path($id); if (!$p || @filemtime($p) < time() - NSD_EXPIRY_SEC) { if ($p) @unlink($p); http_response_code(404); exit; } $ext = strtolower(pathinfo($p, PATHINFO_EXTENSION)); $mime = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif'][$ext] ?? 'application/octet-stream'; header('Access-Control-Allow-Origin: *'); header('Content-Type: ' . $mime); header('Cache-Control: public, max-age=3600'); header('Content-Length: ' . filesize($p)); readfile($p); exit; } if ($api === 'capacity') { $x = @disk_free_space(nsd_base()); nsd_json(['ok' => true, 'free' => ($x === false ? null : (int)$x), 'tiles' => count(glob(nsd_shards() . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: []), 'parity' => nsd_norm_hash(), 'expiry_days' => NSD_EXPIRY_DAYS]); } if ($api === 'mirror') { if ($_SERVER['REQUEST_METHOD'] === 'POST' || isset($_GET['m'])) nsd_mirrors_add((string)($_GET['m'] ?? '') . "\n" . (string)@file_get_contents('php://input')); nsd_json(['ok' => true, 'mirrors' => nsd_mirrors_sample(8)]); } nsd_json(['error' => 'unknown api'], 400); } $wantPanel = isset($_GET['controlpanel']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/controlpanel/?(\?|$)#', (string)$_SERVER['REQUEST_URI'])); if ($wantPanel && (string)($_GET['api'] ?? $_POST['api'] ?? '') === '') { nsp_ensure_site_seed(); nsp_render_controlpanel(); } ?> Free, Instant, No-Signup Dating
No-signup personals
Local first
Map area first
Contact on the card
Black book messages
Distance is yours
Browse nearby
No account wall
Fast and public
CoolGamesCo
No-signup personals
Local first
Map area first
Contact on the card
Black book messages
Distance is yours
Browse nearby
No account wall
Fast and public
CoolGamesCo
Local Queue

Browse Nearby

Matches

Passed

Tap the map to set your area
Tip: Google "My lat long", paste the numbers here.
Black Book

Who matched you

Only people who pressed Match on your card can appear here. You may message them. They cannot message you until you match back — sending a reply does that automatically. No accounts.

Thread

Conversation

Pick someone from your black book to open the thread.
No Signup

Public Profile

Published profiles and photos are public, locked to this browser, and expire after 32 days. Put contact on the profile — that is how people reach you (no account). Clearing this browser key means there is no recovery.

Your Card

No-Signup Date

Map sets the area · Profile publishes a 32-day card. Put contact on the profile — that is how people reach you (no account wall).

No account. Put contact on the profile — that is how people reach you. Map → area · Profile → publish. 18+. Cashflow → nosignup.trade