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 visitor wallet — money/buy 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); define('NSD_MIRROR_JSON_BODY_MAX', 4096); 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; } function nsd_priv_salt() { $f = nsd_logdir() . DIRECTORY_SEPARATOR . '.salt'; if (is_file($f)) { $s = trim((string)@file_get_contents($f)); if ($s !== '') return $s; } try { $s = bin2hex(random_bytes(32)); } catch (Exception $e) { $s = hash('sha256', uniqid('', true) . mt_rand()); } if (@file_put_contents($f, $s, LOCK_EX) !== false) @chmod($f, 0600); return $s; } function nsd_client_tag($ip, $extra = '') { return substr(hash_hmac('sha256', (string)$ip . '|' . $extra, nsd_priv_salt()), 0, 24); } /** kyc/{salted HMAC}/ 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 . nsd_client_tag($ip, 'log'); 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('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 . '|' . nsd_client_tag($ip,'id') . '|' . 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 . nsd_client_tag($ip, 'rate') . '.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 . nsd_client_tag($ip, 'rate') . '.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 . nsd_client_tag($ip, 'rate') . '.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 pseudonymous 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, 'client' => nsd_client_tag($ip, 'event'), 'act' => $act, 'loc' => $loc], JSON_UNESCAPED_SLASHES) . "\n"; if (@file_put_contents($f, $line, FILE_APPEND | LOCK_EX) !== false) @chmod($f, 0600); } /** 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'; } /** P1-DATE-MIRROR-POISON RR233+RR237 + P1-DATE-MIRROR-LOOPBACK-V6 + P1-DATE-MIRROR-MAPPED-CGNAT: * reject RFC2606/reserved/private + bare/mangled hosts; pure IPs use filter_var * NO_PRIV_RANGE|NO_RES_RANGE so expanded/mapped IPv6 loopback and ULA/link-local * cannot enter mirrors.txt (regex only caught literal ::1 / IPv4 private prefixes). * RFC6598 CGNAT 100.64/10 is not always private in filter_var — reject dotted and * IPv4-mapped/expanded embeddings (::ffff:100.64.0.1, ::ffff:6440:1, full form). */ function nsd_mirror_host_ok($host) { /* PHP parse_url keeps brackets on IPv6 literals — strip before any gate */ $host = strtolower(trim((string)$host, " \t[]")); if ($host === '') return false; if (preg_match('/[=\s\/\\\\@\[\]]/', $host)) return false; /* pure IP: public only (closes 0:0:…:1, ::ffff:127.0.0.1, fe80::, fc00::/7, …) */ if (filter_var($host, FILTER_VALIDATE_IP)) { /* RFC6598 CGNAT 100.64/10 first — dotted + mapped/expanded embeddings * (filter_var omits CGNAT; NO_RES may also drop ::ffff: on some PHP builds) */ if (nsd_mirror_is_cgnat_v4($host)) return false; /* IPv4-mapped IPv6: judge the embedded v4 explicitly (live PHP can miss ::ffff:127.0.0.1) */ $emb = nsd_mirror_embedded_v4($host); if ($emb !== null && nsd_mirror_is_private_v4_dotted($emb)) return false; if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return false; } return true; } /* hostname path: legacy private-looking prefixes + RFC2606/example */ if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|::1)/i', $host)) return false; /* Dotted "IP-shaped" hosts that are not filter_var IPs (octal 0177.0.0.1, hex, etc.) */ if (nsd_mirror_is_obfuscated_ip_host($host)) return false; if (preg_match('/\.(example|invalid|test|localhost)$/i', $host)) return false; if (preg_match('/^(example|invalid|test|localhost)$/i', $host)) return false; if (preg_match('/(^|\.)example\.(com|net|org|edu)$/i', $host)) return false; /* chat parity: bare label without dot is not a public mirror host */ if (strpos($host, '.') === false) return false; return true; } /** * Reject IP-looking host labels that bypass FILTER_VALIDATE_IP (octal/hex/partial). * P1-DATE-MIRROR-OCTAL-LOOPBACK: 0177.0.0.1 must not store as a "hostname". */ function nsd_mirror_is_obfuscated_ip_host($host) { $host = strtolower(trim((string)$host, " \t[]")); if ($host === '' || strpos($host, '.') === false) return false; /* four dotted labels, each decimal or 0-leading octal-ish */ if (!preg_match('/^([0-9a-fx]+)(\.([0-9a-fx]+)){3}$/i', $host)) return false; /* pure decimal without leading zeros is a normal IPv4 — handled above via filter_var */ if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; /* any 0-leading octet (0177) or hex marker → treat as obfuscated IP, never a public DNS name */ if (preg_match('/(^|\.)0[0-9]+/', $host)) return true; if (preg_match('/0x/i', $host)) return true; /* all-numeric dotted but invalid as IPv4 (e.g. 999.1.1.1) still not a domain we want as mirror */ if (preg_match('/^[0-9]+(\.[0-9]+){3}$/', $host)) return true; return false; } /** True if $ip is dotted RFC6598 CGNAT or an IPv4-mapped/expanded IPv6 embedding of one. */ function nsd_mirror_is_cgnat_v4($ip) { $ip = strtolower((string)$ip); if (preg_match('/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./', $ip)) return true; $emb = nsd_mirror_embedded_v4($ip); if ($emb !== null && preg_match('/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./', $emb)) return true; return false; } /** Extract dotted IPv4 from ::ffff:0:0/96 mapped forms; null if not mapped. */ function nsd_mirror_embedded_v4($ip) { $ip = strtolower(trim((string)$ip, " \t[]")); $bin = @inet_pton($ip); if ($bin === false || strlen($bin) !== 16) return null; if (strncmp($bin, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 12) !== 0) return null; return ord($bin[12]) . '.' . ord($bin[13]) . '.' . ord($bin[14]) . '.' . ord($bin[15]); } /** * True if dotted IPv4 is loopback/unspecified/RFC1918/link-local/CGNAT. * Used because some PHP builds treat ::ffff:127.0.0.1 as "public" under FILTER_FLAG_NO_PRIV_RANGE. */ function nsd_mirror_is_private_v4_dotted($ip) { $ip = strtolower(trim((string)$ip)); if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; if (preg_match('/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./', $ip)) return true; if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return true; $long = ip2long($ip); if ($long === false) return true; $u = sprintf('%u', $long); /* 0.0.0.0/8 unspecified, 127.0.0.0/8 loopback (belt if flags ever skip) */ if ($u < 16777216) return true; if ($u >= 2130706432 && $u <= 2147483647) return true; return false; } function nsd_mirror_ok($u) { $u = trim((string)$u); if ($u === '') return null; /* RR form-host parity (chat RR372 / work RR373): strip m|url|host= so body host=nosignup.com is a hostname, not https://host=… */ if (preg_match('/^(?:m|url|host)=/i', $u)) { $u = preg_replace('/^(?:m|url|host)=/i', '', $u); $u = rawurldecode($u); } $u = rawurldecode(trim($u)); if ($u === '' || preg_match('/[=\s]/', $u) && !preg_match('#^https?://#i', $u)) return null; if (!preg_match('#^https?://#i', $u)) $u = 'https://' . $u; if (strlen($u) > 220) return null; /* double-encoded / nested scheme wrap (https://https%3A%2F%2F…) */ if (preg_match('#^https?://https?%3a%2f%2f#i', $u) || preg_match('#^https?://https?://#i', $u)) return null; /* Unbracketed IPv6 / IPv4-mapped authority is ambiguous: parse_url splits the last * :hextet as a port (e.g. ::ffff:6440:1 → host ::ffff:6440 + port 1), then the * truncated host can pass public IP gates. Require [brackets] for multi-colon hosts. */ if (preg_match('~^https?://([^/?#]+)~i', $u, $am)) { $auth = $am[1]; if (strpos($auth, '@') !== false) { $auth = substr($auth, strrpos($auth, '@') + 1); } if ($auth !== '' && $auth[0] !== '[' && substr_count($auth, ':') >= 2) { return null; } } $p = @parse_url($u); if (!$p || empty($p['host'])) return null; $scheme = strtolower($p['scheme'] ?? 'https'); if ($scheme !== 'http' && $scheme !== 'https') return null; /* parse_url may keep [brackets] on IPv6; normalize before gates and storage */ $host = strtolower(trim(rawurldecode($p['host']), " \t[]")); if (strpos($host, '%') !== false) return null; if (!nsd_mirror_host_ok($host)) return null; /* nested scheme in path/query = mangle / poison wrap */ $rest = ($p['path'] ?? '') . (isset($p['query']) ? '?' . $p['query'] : ''); if (preg_match('#https?://#i', $rest) || preg_match('#https?%3a%2f%2f#i', $rest)) return null; /* P1-DATE-MIRROR-PORT-443-PUBLIC: drop default scheme ports so :443/:80 are not stored as malformed host forms */ $pn = isset($p['port']) ? (int)$p['port'] : 0; $port = ($pn > 0 && !(($scheme === 'https' && $pn === 443) || ($scheme === 'http' && $pn === 80))) ? (':' . $pn) : ''; $path = $p['path'] ?? ''; /* Re-bracket IPv6 literals for a parseable stored URL */ $hostOut = (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) ? ('[' . $host . ']') : $host; return rtrim($scheme . '://' . $hostOut . $port . $path, '/'); } 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; } /** @return array{accepted:int,rejected:int,changed:bool} RR340 honest poison accounting */ function nsd_mirrors_add($txt) { $cur = nsd_mirrors_read(); $now = time(); $changed = false; $accepted = 0; $rejected = 0; $nTok = 0; foreach (preg_split('/[\s,]+/', (string)$txt) ?: [] as $raw) { $raw = trim((string)$raw); if ($raw === '') continue; $nTok++; if ($nTok > 32) break; $u = nsd_mirror_ok($raw); if ($u === null) { $rejected++; continue; } $cur[$u] = $now; $changed = true; $accepted++; } if ($changed) { 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); } return ['accepted' => $accepted, 'rejected' => $rejected, 'changed' => $changed]; } function nsd_mirror_field_cap($s) { $s = (string)$s; if (strlen($s) > NSD_MIRROR_JSON_BODY_MAX) { nsd_json(['ok' => false, 'err' => 'Body too large'], 413); } return $s; } /** CHAT-2: fail-closed mirror ingest (9.php fun_json_body_read / 4.php nst_device_wallet_read_json_body). Never substr partial JSON. */ function nsd_mirror_body_read() { $declared = trim((string)($_SERVER['CONTENT_LENGTH'] ?? '')); if ($declared !== '') { if (!ctype_digit($declared) || strlen($declared) > 9 || (int)$declared > NSD_MIRROR_JSON_BODY_MAX) { nsd_json(['ok' => false, 'err' => 'Body too large'], 413); } } $fh = @fopen('php://input', 'rb'); if (!is_resource($fh)) { return ''; } $raw = @stream_get_contents($fh, NSD_MIRROR_JSON_BODY_MAX + 1); @fclose($fh); if (!is_string($raw)) { return ''; } if (strlen($raw) > NSD_MIRROR_JSON_BODY_MAX) { nsd_json(['ok' => false, 'err' => 'Body too large'], 413); } return $raw; } function nsd_mirrors_sample($n) { $u = array_keys(nsd_mirrors_read()); if (!$u) return []; shuffle($u); return array_slice($u, 0, $n); } /* ======================================================================== * WHO-KEPT-YOU + PAIR THREADS (DATE-MATCH) * Identity = browser-owned nsd_owner key (hashed server-side). No accounts. * Semantics (operator law — implement exactly): * KEEP A→B = record so B's Who-Kept-you list lists A; A still cannot message B. * WHO-KEPT = people who Kept ME (actionable message-gates only). * SEND msg = append pair .txt + auto-Keep 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 Who-Kept-you 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 Keep records targeting $to_owner_hash (their Who-Kept-you list). */ 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 Keep FROM $from_owner (raw key) TO profile $to_profile_id. * Stored under the TARGET's owner_hash so it appears in THEIR Who-Kept-you list. * 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' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'code' => 400]; /* RR343 P1-DATE-KEEP-MSG-ERR-PLAIN: opaque keep errors → stranger plain English */ if (!nsd_id_ok($to_profile_id)) return ['error' => 'That profile id looks invalid — open a card from the board and try Keep again', 'code' => 400]; $found = nsd_find_profile($to_profile_id); if (!$found) return ['error' => 'That profile is gone or expired — refresh the board', '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' => 'That card has no owner key — it cannot receive Keeps', 'code' => 400]; $from_oh = nsd_owner_hash($from_owner); if ($from_oh === $to_oh) return ['error' => 'You cannot Keep your own card', 'code' => 400]; /* Prefer caller's published profile id for the Who-Kept-you card. */ $from_pid = nsd_owner_index_get($from_oh); if ($from_pid === null) { /* Fallback: scan is avoided — require a public card before Keep. */ return ['error' => 'Publish your own profile before you can Keep someone', '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' => 'Could not encode keep record — try again', 'code' => 500]; if (@file_put_contents(nsd_match_file($to_oh, $from_oh), $json, LOCK_EX) === false) { /* RR365 P1-DATE-STORAGE-PLAIN */ return ['error' => 'Could not save — try again in a moment', '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; } /** * Who-Kept-you list for $owner (raw key) = people who Kept ME. * Returns public profile cards + peer_profile_id for messaging. */ function nsd_blackbook_list($owner) { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', '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 Who-Kept-you list (they must have Kept 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 Who-Kept-you list. */ function nsd_thread_read($owner, $peer_profile_id) { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', '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 Who-Kept-you list — they must Keep 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 Who-Kept-you list (they Kept me). * Side effect: AUTO-KEEP 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' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'code' => 400]; $text = nsd_san($text, 500); /* RR343 P1-DATE-KEEP-MSG-ERR-PLAIN */ if ($text === '') return ['error' => 'Type a message before sending', '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 Who-Kept-you list — they must Keep you first', 'code' => 403]; /* AUTO-KEEP 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 Keep file targeting peer (so I appear in their Who-Kept-you list). */ $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) { /* RR365 P1-DATE-STORAGE-PLAIN */ return ['error' => 'Could not save the message — try again in a moment', 'code' => 500]; } return nsd_thread_read($owner, $peer_profile_id); } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { $api = (string)($_GET['api'] ?? ''); $read = isset($_GET['src']) || isset($_GET['download']) || $api === 'media'; if ($read) { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: Content-Type'); } http_response_code(204); exit; } if (isset($_GET['src']) || isset($_GET['download'])) { 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; } /* EMPIRE SETUP GATE → trade console until filled (api/src/download/panel exempt) */ (function () { $api = (string)($_GET['api'] ?? $_POST['api'] ?? ''); if ($api !== '') return; if (isset($_GET['src']) || isset($_GET['download']) || isset($_GET['controlpanel']) || isset($_GET['empire_setup']) || isset($_GET['setup'])) return; $uri = (string)($_SERVER['REQUEST_URI'] ?? ''); if (preg_match('#/(controlpanel|setup)/?(\?|$)#', $uri)) return; $dataDir = __DIR__ . DIRECTORY_SEPARATOR . 'data'; $ok = false; $local = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup.json'; if (is_file($local)) { $lj = json_decode((string)@file_get_contents($local), true); $ok = is_array($lj) && !empty($lj['filled']); } $cache = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup_status_cache.json'; if (!$ok && is_file($cache)) { $c = json_decode((string)@file_get_contents($cache), true); if (is_array($c) && isset($c['ts'], $c['filled']) && (time() - (int)$c['ts']) < 60 && !empty($c['filled'])) $ok = true; } if (!$ok) { $ctx = stream_context_create(['http' => ['timeout' => 2.5, 'ignore_errors' => true]]); $httpsOn = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') || ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) || (strtolower((string)($_SERVER['REQUEST_SCHEME'] ?? '')) === 'https') || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https'); $scheme = $httpsOn ? 'https' : 'http'; $rawS = @file_get_contents($scheme . '://nosignup.trade/?api=empire_setup_status', false, $ctx); $httpsHit = false; if (is_string($rawS) && $rawS !== '') { $j = json_decode($rawS, true); if (is_array($j) && array_key_exists('filled', $j)) { $httpsHit = true; $ok = !empty($j['filled']); if (!is_dir($dataDir)) @mkdir($dataDir, 0755, true); @file_put_contents($cache, json_encode(['ts' => time(), 'filled' => $ok]) . "\n", LOCK_EX); } } if (!$httpsHit && !$ok && is_file($cache)) { $c = json_decode((string)@file_get_contents($cache), true); if (is_array($c) && !empty($c['filled'])) $ok = true; } } if (!$ok) { header('Cache-Control: no-store'); header('Location: //nosignup.trade/?empire_setup=1', true, 302); 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); } /* RR329 P1-DATE-LIST-ALIAS: list → profiles (not opaque unknown api) */ if ($api === 'list') { $api = 'profiles'; } if ($api === 'profiles') { $tile = $_GET['tile'] ?? ''; /* RR320 P1-DATE-TILE-ERR-PLAIN · RR358 schema ok:false+err (face still reads error) */ if (!nsd_tile_ok($tile)) nsd_json(['ok' => false, 'err' => 'Pick a map tile (location) before browsing profiles', 'error' => 'Pick a map tile (location) before browsing profiles'], 400); $radius = max(1, min(7, (int)($_GET['radius'] ?? 1))); $remote = !empty($_GET['remote']) && $_GET['remote'] !== '0' && $_GET['remote'] !== 'false'; 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(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); /* RR355 P1-DATE-POST-ERR-SCHEMA: post_profile validation includes ok:false + err */ if ($owner === '') nsd_json(['ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)'], 400); $tile = $_POST['tile'] ?? ''; /* RR320 P1-DATE-TILE-ERR-PLAIN */ if (!nsd_tile_ok($tile)) nsd_json(['ok' => false, 'err' => 'Pick a map tile (location) before posting a profile', 'error' => 'Pick a map tile (location) before posting a profile'], 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(['ok' => false, 'err' => 'name, 18+ age, and contact required — contact on the profile is how people reach you (no account)', '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(['ok' => false, 'err' => 'profile rejected by bio_risk brain', 'error' => 'profile rejected by bio_risk brain', 'brains' => ['bio_risk' => $bioRisk, 'name_sane' => $nameSane]], 400); } if (($nameSane['score'] ?? 0) >= 0.9) { nsd_json(['ok' => false, 'err' => 'name rejected by name_sane brain', '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) { /* RR351 P1-DATE-PROFILE-LOCKED-PLAIN */ if (($existing[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json(['error' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'ok' => false, 'err' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)'], 403); } else { $rl = nsd_rate_check($ip, 'profile', $uid); if ($rl['wait'] > 0) nsd_json(['ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', '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; /* RR365 P1-DATE-STORAGE-PLAIN: storage fail → visitor plain English */ if (!nsd_put_profile($data)) nsd_json(['ok' => false, 'err' => 'Could not save the profile — try again in a moment', 'error' => 'Could not save the profile — try again in a moment'], 500); /* Owner index: reverse map for Who-Kept-you 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(['ok' => false, 'err' => 'POST only', '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]); /* RR351 P1-DATE-PROFILE-LOCKED-PLAIN */ if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json(['error' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'ok' => false, 'err' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)'], 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: Keep gate + Who-Kept-you list + pair threads ---- */ if ($api === 'record_match') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); $target = (string)($_POST['target_id'] ?? $_POST['id'] ?? ''); /* RR358 P1-DATE-GATE-ERR-SCHEMA: owner/rate/core fail include ok:false+err */ if ($owner === '') nsd_json(['ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)'], 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(['ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', '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'])) { $em = (string)($r['error'] ?? 'Keep failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (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 Keep you.']); } if ($api === 'blackbook' || $api === 'list_matched_me') { /* POST owner — lists only people who matched ME. GET without owner → 400. */ $owner = nsd_owner($_POST['owner'] ?? ''); if ($owner === '') nsd_json(['ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)'], 400); $wait = nsd_browse_check($ip); if ($wait > 0) nsd_json(['ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', 'error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy'], 429); $r = nsd_blackbook_list($owner); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'blackbook failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_log('blackbook', 'n=' . (int)($r['count'] ?? 0)); nsd_json($r); } if ($api === 'thread_get') { $owner = nsd_owner($_POST['owner'] ?? ''); $peer = (string)($_POST['peer_id'] ?? $_GET['peer_id'] ?? ''); if ($owner === '') nsd_json(['ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)'], 400); $r = nsd_thread_read($owner, $peer); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'thread failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_json($r); } if ($api === 'thread_append') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', '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(['ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)'], 400); /* Soft IP bucket: up to 60 messages / 5 min. */ $wait = nsd_count_rate($ip, 'msg', 60); if ($wait > 0) nsd_json(['ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', 'error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy'], 429); unset($uid); $r = nsd_thread_append_core($owner, $peer, $text); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'send failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_log('thread_append', 'peer=' . $peer); nsd_json($r); } if ($api === 'upload_photo') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $id = $_POST['id'] ?? ''; $owner = nsd_owner($_POST['owner'] ?? ''); $found = nsd_find_profile($id); /* RR351 P1-DATE-PROFILE-LOCKED-PLAIN */ if (!$found) nsd_json(['error' => 'That profile is gone or expired — refresh the board before uploading a photo', 'ok' => false, 'err' => 'That profile is gone or expired — refresh the board before uploading a photo'], 404); if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json(['error' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'ok' => false, 'err' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)'], 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' => 'Choose a photo file to upload', 'ok' => false, 'err' => 'Choose a photo file to upload'], 400); if ((int)($_FILES['photo']['size'] ?? 0) <= 0 || (int)$_FILES['photo']['size'] > NSD_MAX_UPLOAD_BYTES) nsd_json(['error' => 'Photo is too large for this host — try a smaller image', 'ok' => false, 'err' => 'Photo is too large for this host — try a smaller image'], 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') { /* RR348 P1-DATE-MEDIA-MISS-PLAIN: missing/expired photo → JSON not empty HTML 404 */ $id = (string)($_GET['id'] ?? ''); if ($id === '') nsd_json(['ok' => false, 'err' => 'Need a profile photo id (?api=media&id=...)', 'error' => 'Need a profile photo id (?api=media&id=...)'], 400); $p = nsd_media_path($id); if (!$p || @filemtime($p) < time() - NSD_EXPIRY_SEC) { if ($p) @unlink($p); nsd_json(['ok' => false, 'err' => 'Photo not found or expired', 'error' => 'Photo not found or expired'], 404); } $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') { nsd_json(['ok' => true, 'tiles' => count(glob(nsd_shards() . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: []), 'parity' => nsd_norm_hash(), 'expiry_days' => NSD_EXPIRY_DAYS]); } if ($api === 'mirror') { /* RR340 + form-host parity (chat/work): prefer m|host|url form fields; never treat raw "host=…" as a hostname */ $add = ['accepted' => 0, 'rejected' => 0, 'changed' => false]; /* P1-DATE-MIRROR-GET-METHOD: GET is sample-only JSON (never write). * Mirror ingest (accepted/rejected) is POST-only. */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $raw = ''; if (isset($_POST['m']) && (string)$_POST['m'] !== '') { $raw = nsd_mirror_field_cap(is_array($_POST['m']) ? implode("\n", $_POST['m']) : (string)$_POST['m']); } elseif (isset($_POST['host']) && (string)$_POST['host'] !== '') { $raw = nsd_mirror_field_cap((string)$_POST['host']); } elseif (isset($_POST['url']) && (string)$_POST['url'] !== '') { $raw = nsd_mirror_field_cap((string)$_POST['url']); } else { $rawIn = nsd_mirror_body_read(); if (stripos((string)($_SERVER['CONTENT_TYPE'] ?? ''), 'application/json') !== false) { $jd = json_decode($rawIn, true); if (is_array($jd) && isset($jd['m'])) $raw = (string)$jd['m']; elseif (is_array($jd) && isset($jd['url'])) $raw = (string)$jd['url']; elseif (is_array($jd) && isset($jd['host'])) $raw = (string)$jd['host']; } elseif (preg_match('/(?:^|&)(?:m|host|url)=([^&]*)/i', $rawIn, $mm)) { $raw = rawurldecode($mm[1]); } else { $raw = $rawIn; } } if ($raw !== '') $add = nsd_mirrors_add($raw); } elseif (!in_array(strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')), ['GET', 'HEAD'], true)) { nsd_json(['ok' => false, 'err' => 'method not allowed', 'allow' => 'GET, POST', 'error' => 'method not allowed'], 405); } nsd_json([ 'ok' => true, 'mirrors' => nsd_mirrors_sample(8), 'accepted' => (int)$add['accepted'], 'rejected' => (int)$add['rejected'], 'law' => 'Private/RFC2606/example hosts rejected — never sampled', ]); } /* RR337 P1-DATE-UNKNOWN-API-PLAIN: route hints + ok:false (not bare unknown api) */ nsd_json([ 'ok' => false, 'err' => 'Unknown date API - use selfhash, profiles, list, post_profile, delete_profile, record_match, blackbook, list_matched_me, thread_get, thread_append, upload_photo, media, capacity, mirror, or open the personals face', 'api' => $api, 'error' => 'Unknown date API - use selfhash, profiles, list, post_profile, delete_profile, record_match, blackbook, list_matched_me, thread_get, thread_append, upload_photo, media, capacity, mirror, or open the personals face', ], 404); } $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(); } ?> No-Signup Personals · map + contact (not a dating app)
No-signup personals
Local first
Map area first
Contact on the card
Messages after Keep
Distance is yours
Browse nearby
No account wall
Fast and public
CoolGamesCo
No-signup personals
Local first
Map area first
Contact on the card
Messages after Keep
Distance is yours
Browse nearby
No account wall
Fast and public
CoolGamesCo
Local Queue

Browse Nearby

Saved (local)

Skipped

Tap the map to set your area
Tip: Google "My lat long", paste the numbers here.
Who Kept you

Who kept your card

Only people who pressed Keep on your card can appear here. You may message them. They cannot message you until you Keep them back — sending a reply does that automatically. Direct contact still lives on each card. No accounts.

Thread

Conversation

Pick someone who Kept your card 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 Personals

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+. Money/buy → nosignup.trade