'morse', 'kind' => 'sketch', 'label' => 'Morse (binaural trainer)', 'href' => '?sketch=morse'], ['id' => 'asl', 'kind' => 'sketch', 'label' => 'ASL (binaural skill deck)', 'href' => '?sketch=asl'], // Add more custom sites here: ['id'=>'x','kind'=>'sketch','label'=>'…','href'=>'?sketch=x'], ]; } /** NSU empire products always in the ? pool (equal weight with sketches). */ function nsp_product_pool(): array { return [ ['id' => 'trade', 'kind' => 'url', 'label' => 'Trade', 'href' => 'https://nosignup.trade/'], ['id' => 'chat', 'kind' => 'url', 'label' => 'Chat', 'href' => 'https://nosignup.chat/'], ['id' => 'work', 'kind' => 'url', 'label' => 'Work', 'href' => 'https://nosignup.work/'], ['id' => 'market', 'kind' => 'url', 'label' => 'Market', 'href' => 'https://nosignup.market/'], ['id' => 'date', 'kind' => 'url', 'label' => 'Date', 'href' => 'https://nosignup.date/'], ['id' => 'fun', 'kind' => 'url', 'label' => 'Fun', 'href' => 'https://nosignup.fun/'], ['id' => 'info', 'kind' => 'url', 'label' => 'Info', 'href' => 'https://nosignup.info/'], ]; } function nsp_hub_random_pool(): array { return array_merge(nsp_product_pool(), nsp_sketch_catalog()); } /* ---- SITE-LOCAL CONTROL PANEL (renter key; not OS root) ---- */ function nsp_vault_dir(): string { $sib = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vault'; $loc = __DIR__ . DIRECTORY_SEPARATOR . 'vault'; foreach ([$sib, $loc] as $d) { if (is_dir($d) || @mkdir($d, 0700, true)) { if (is_dir($d) && is_writable($d)) return $d; } } return $sib; } function nsp_data_dir(): string { $d = __DIR__ . DIRECTORY_SEPARATOR . 'data'; if (!is_dir($d)) @mkdir($d, 0755, true); $ht = $d . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); return $d; } function nsp_hash_file(): string { return nsp_data_dir() . DIRECTORY_SEPARATOR . 'admin.pass.hash'; } function nsp_seed_file(): string { return nsp_data_dir() . DIRECTORY_SEPARATOR . 'site.seed'; } function nsp_norm_seed(string $s): string { return strtolower(trim(preg_replace('/\s+/', ' ', $s) ?? '')); } /** Identity surface only (same scheme as trade); hub has no visitor spend ledger. */ function nsp_addr_from_seed(string $seed): string { return hash('sha256', 'nsu-addr-v1|' . nsp_norm_seed($seed)); } /** * 12-word site seed (not BIP39). CSPRNG into fixed word pool. * Panel unlock for THIS host only — not a faucet mint (hub has no visitor NSU). */ function nsp_generate_site_seed(): string { static $wl = [ 'able','acid','aged','also','aqua','arch','area','army','atom','aunt','auto','avoid', 'axis','baby','band','bank','bare','barn','base','bean','bear','belt','bike','bind', 'bird','bite','blue','boat','body','bold','bolt','bone','book','boot','born','bowl', 'brass','brave','bread','brick','brief','bring','broad','broke','brown','brush','build','bulk', 'burn','burst','bush','busy','cable','cage','cake','calm','camp','cane','cape','card', 'care','cart','case','cash','cast','cave','cell','cent','chat','chef','chin','chip', 'city','clap','clay','clip','club','coal','coat','code','coil','coin','cold','come', 'cook','cool','cope','copy','cord','core','corn','cost','cove','crab','crew','crop', 'crow','cube','cult','curb','cure','curl','dark','dart','dash','data','dawn','deal', 'dear','deck','deep','deer','desk','dial','dice','diet','dine','dirt','disc','dock', 'dome','done','door','dose','down','draw','drip','drop','drum','dual','duck','dune', 'dusk','dust','duty','each','earn','east','easy','echo','edge','edit','else','emit', 'epic','even','ever','evil','exit','face','fact','fade','fail','fair','fall','fame', 'farm','fast','fate','fear','feed','feel','fern','file','fill','film','find','fine', 'fire','firm','fish','flag','flat','flee','flip','flow','foam','foil','fold','font', 'food','fool','foot','ford','fork','form','fort','foul','four','free','frog','from', 'fuel','full','fund','fuse','gain','game','gate','gear','gene','gift','girl','give', 'glad','glow','glue','goal','goat','gold','golf','good','grab','grad','gram','gray', 'grid','grim','grin','grip','grow','gulf','guru','hail','hair','half','hall','hand', 'hang','hard','harm','harp','hate','have','hawk','haze','head','heal','heap','heat', 'heed','heel','held','help','herb','here','hero','hide','high','hill','hint','hire', 'hold','hole','home','hood','hook','hope','horn','host','hour','huge','hull','hung', 'hunt','hurt','icon','idea','idle','inch','info','into','iron','item','jade','jail', 'jazz','join','joke','jump','june','jury','just','keen','keep','kept','kick','kind', 'king','kite','knee','knew','knit','knot','know','lace','lack','lady','lake','lamp', 'land','lane','last','late','lava','lawn','lead','leaf','lean','left','lend','lens', ]; $n = count($wl); $bytes = random_bytes(12); $out = []; for ($i = 0; $i < 12; $i++) { $out[] = $wl[ord($bytes[$i]) % $n]; } return implode(' ', $out); } /** Owner-only vault note: site seed = panel unlock for THIS host. Never to renters. */ function nsp_vault_site_seed_note(string $seed): void { $d = nsp_vault_dir(); if (!is_dir($d) && !@mkdir($d, 0700, true)) { return; } @chmod($d, 0700); $v = nsp_hub_variant(); $body = 'NOSIGNUP.' . strtoupper($v) . " HUB SITE WALLET SEED (OWNER ONLY)\n" . "This seed unlocks /controlpanel for THIS host only (com/org/net each own vault+data).\n" . "Hub has no visitor NSU ledger or mint on this crop. NO RECOVERY.\n" . "Renters must NOT receive this secret.\n" . "Generated: " . gmdate('c') . "\n\n" . trim($seed) . "\n"; @file_put_contents($d . DIRECTORY_SEPARATOR . 'SITE_WALLET_SEED.txt', $body, LOCK_EX); @chmod($d . DIRECTORY_SEPARATOR . 'SITE_WALLET_SEED.txt', 0600); } /** True if $seed matches data/site.seed (normalized). */ function nsp_panel_seed_ok(string $seed): bool { $seed = nsp_norm_seed($seed); if ($seed === '') { return false; } $path = nsp_seed_file(); if (!is_file($path)) { return false; } $have = nsp_norm_seed((string)@file_get_contents($path)); if ($have === '') { return false; } return hash_equals($have, $seed); } /** * Ensure data/site.seed exists; mirror to vault SITE_WALLET_SEED.txt on first write. * Idempotent. Call before admin API so setup is never land-grabable. * NOT a treasury faucet mint — hub has no visitor ledger this crop. */ function nsp_ensure_site_seed(): void { $path = nsp_seed_file(); if (is_file($path) && nsp_norm_seed((string)@file_get_contents($path)) !== '') { $vd = nsp_vault_dir(); $note = $vd . DIRECTORY_SEPARATOR . 'SITE_WALLET_SEED.txt'; if (!is_file($note) || trim((string)@file_get_contents($note)) === '') { nsp_vault_site_seed_note(nsp_norm_seed((string)@file_get_contents($path))); } return; } $seed = nsp_generate_site_seed(); file_put_contents($path, $seed . "\n", LOCK_EX); @chmod($path, 0600); nsp_vault_site_seed_note($seed); } function nsp_vault_write(string $plain): void { $d = nsp_vault_dir(); @chmod($d, 0700); $v = nsp_hub_variant(); @file_put_contents($d . DIRECTORY_SEPARATOR . 'README.txt', 'NOSIGNUP.' . strtoupper($v) . " HUB VAULT - ROOT/OPERATOR ONLY\n" . "Site seed unlocks /controlpanel (SITE_WALLET_SEED.txt + data/site.seed).\n" . "Legacy admin password (optional migrate): ADMIN_PASSWORD.txt\n" . "No visitor NSU ledger on MAP. " . gmdate('c') . "\n", LOCK_EX); @file_put_contents($d . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt', $plain . "\n", LOCK_EX); @chmod($d . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt', 0600); $ht = $d . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) @file_put_contents($ht, "Require all denied\nDeny from all\n"); } function nsp_json(array $x, int $c = 200): void { http_response_code($c); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: no-store'); echo json_encode($x, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); exit; } /* ---- MORSE-v2 organism telemetry (ethos §11–12; NOT accounts) ---- */ const NSP_MORSE_MAX_POST = 1048576; const NSP_MORSE_MASTERY_EXPOSURE_BUDGET = 50; const NSP_MORSE_MASTERY_ATTEMPT_BUDGET = 12; const NSP_MORSE_MASTERY_WINDOW = 5; const NSP_MORSE_MASTERY_REQUIRED = 4; const NSP_MORSE_MASTERY_MAX_MEDIAN_MS = 2500; const NSP_MORSE_MAX_RECENT = 800; function nsp_morse_data_dir(): string { $d = nsp_data_dir() . DIRECTORY_SEPARATOR . 'morse'; if (!is_dir($d)) { @mkdir($d, 0700, true); } if (is_dir($d)) { @chmod($d, 0700); $ht = $d . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($ht)) { @file_put_contents($ht, "Require all denied\nDeny from all\n", LOCK_EX); } $idx = $d . DIRECTORY_SEPARATOR . 'index.html'; if (!is_file($idx)) { @file_put_contents($idx, "", LOCK_EX); } } return $d; } function nsp_morse_salt(): string { $f = nsp_morse_data_dir() . DIRECTORY_SEPARATOR . '.salt'; if (!is_file($f)) { @file_put_contents($f, bin2hex(random_bytes(32)), LOCK_EX); @chmod($f, 0600); } $s = trim((string)@file_get_contents($f)); if ($s === '') { $s = bin2hex(random_bytes(32)); @file_put_contents($f, $s, LOCK_EX); @chmod($f, 0600); } return $s; } /** HMAC(IP|UA, salt) — raw IP never written. */ function nsp_morse_visitor_hash(): string { $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); $ua = (string)($_SERVER['HTTP_USER_AGENT'] ?? ''); return substr(hash_hmac('sha256', $ip . '|' . $ua, nsp_morse_salt()), 0, 32); } function nsp_morse_ua_hash(): string { $ua = (string)($_SERVER['HTTP_USER_AGENT'] ?? ''); return substr(hash_hmac('sha256', $ua, nsp_morse_salt()), 0, 16); } function nsp_morse_clean($v, int $max = 200): string { $s = is_scalar($v) ? (string)$v : ''; $s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $s) ?? ''; return substr($s, 0, $max); } function nsp_morse_rows_file(): string { return nsp_morse_data_dir() . DIRECTORY_SEPARATOR . 'visitor_stats.jsonl'; } function nsp_morse_read_rows($fh): array { rewind($fh); $rows = []; while (($line = fgets($fh)) !== false) { $line = rtrim($line, "\r\n"); if ($line === '' || !str_contains($line, "\t")) continue; [$id, $json] = explode("\t", $line, 2); $id = preg_replace('/[^a-f0-9]/', '', $id) ?? ''; if ($id === '') continue; $decoded = json_decode($json, true); if (is_array($decoded)) $rows[$id] = $decoded; } return $rows; } function nsp_morse_write_rows($fh, array $rows): void { ftruncate($fh, 0); rewind($fh); foreach ($rows as $id => $row) { $id = preg_replace('/[^a-f0-9]/', '', (string)$id) ?? ''; if ($id === '') continue; fwrite($fh, $id . "\t" . json_encode($row, JSON_UNESCAPED_SLASHES) . "\n"); } fflush($fh); @chmod(nsp_morse_rows_file(), 0600); } /** * @param bool $hard if true, API-style 500 exit on open/write failure; if false, return null (fail-open for page render). */ function nsp_morse_with_rows(callable $fn, bool $hard = true) { nsp_morse_data_dir(); $fh = @fopen(nsp_morse_rows_file(), 'c+'); if (!$fh) { if ($hard) nsp_json(['ok' => false, 'err' => 'morse stats open fail'], 500); return null; } flock($fh, LOCK_EX); try { $rows = nsp_morse_read_rows($fh); $result = $fn($rows); if (is_array($result) && array_key_exists('__rows', $result)) { nsp_morse_write_rows($fh, $result['__rows']); unset($result['__rows']); } flock($fh, LOCK_UN); fclose($fh); return $result; } catch (Throwable $e) { flock($fh, LOCK_UN); fclose($fh); if ($hard) nsp_json(['ok' => false, 'err' => 'morse stats write fail'], 500); return null; } } function nsp_morse_safe_snapshot(array $s): array { return [ 'assignedCondition' => nsp_morse_clean($s['assignedCondition'] ?? '', 8), 'morseWpm' => max(5, min(40, (float)($s['morseWpm'] ?? 18))), 'spinEnabled' => !empty($s['spinEnabled']), 'spinSweepEnabled' => !empty($s['spinSweepEnabled']), 'binauralEnabled' => !empty($s['binauralEnabled']), 'spreadSweepEnabled' => !empty($s['spreadSweepEnabled']), 'lowOscillatorEnabled' => !empty($s['lowOscillatorEnabled']), 'lowOscillatorSweepEnabled' => !empty($s['lowOscillatorSweepEnabled']), 'spinHz' => round(max(0, min(20, (float)($s['spinHz'] ?? 0))), 3), 'spreadHz' => round(max(0, min(40, (float)($s['spreadHz'] ?? 0))), 3), 'lowOscHz' => round(max(0, min(20, (float)($s['lowOscHz'] ?? 0))), 3), 'fieldVolume' => round(max(0, min(100, (float)($s['fieldVolume'] ?? 0))), 3), 'movementDepth' => round(max(0, min(100, (float)($s['movementDepth'] ?? 0))), 3), ]; } function nsp_morse_safe_event(array $e): ?array { $event = nsp_morse_clean($e['event'] ?? '', 40); if (!in_array($event, ['heard', 'test-answer', 'mastery'], true)) return null; $char = strtolower(nsp_morse_clean($e['char'] ?? '', 1)); if (!preg_match('/^[a-z0-9]$/', $char)) return null; $out = [ 'at' => nsp_morse_clean($e['at'] ?? '', 40), 'event' => $event, 'mode' => nsp_morse_clean($e['mode'] ?? '', 20), 'char' => $char, 'snapshot' => nsp_morse_safe_snapshot(is_array($e['snapshot'] ?? null) ? $e['snapshot'] : []), ]; if ($event === 'test-answer') { $answer = strtolower(nsp_morse_clean($e['answer'] ?? '', 1)); $out['answer'] = preg_match('/^[a-z0-9]$/', $answer) ? $answer : ''; $out['correct'] = !empty($e['correct']); $out['responseMs'] = max(0, min(600000, (int)($e['responseMs'] ?? 0))); } if ($event === 'mastery') { $out['exposuresToMastery'] = max(0, min(100000, (int)($e['exposuresToMastery'] ?? 0))); $out['attemptsToMastery'] = max(0, min(100000, (int)($e['attemptsToMastery'] ?? 0))); $out['msToMastery'] = max(0, min(31536000000, (int)($e['msToMastery'] ?? 0))); $out['medianResponseMsAtMastery'] = max(0, min(600000, (int)($e['medianResponseMsAtMastery'] ?? 0))); $out['successWithinBudget'] = !empty($e['successWithinBudget']); } return $out; } function nsp_morse_safe_chars(array $stats): array { $chars = is_array($stats['chars'] ?? null) ? $stats['chars'] : []; $out = []; foreach ($chars as $char => $c) { $char = strtolower(nsp_morse_clean($char, 1)); if (!preg_match('/^[a-z0-9]$/', $char) || !is_array($c)) continue; $heard = max(0, min(1000000, (int)($c['heard'] ?? 0))); $testAttempts = max(0, min(1000000, (int)($c['testAttempts'] ?? 0))); $mastered = !empty($c['mastered']); $out[$char] = [ 'heard' => $heard, 'practiceHeard' => max(0, min(1000000, (int)($c['practiceHeard'] ?? 0))), 'testAttempts' => $testAttempts, 'correct' => max(0, min(1000000, (int)($c['correct'] ?? 0))), 'wrong' => max(0, min(1000000, (int)($c['wrong'] ?? 0))), 'mastered' => $mastered, 'eligibleForSuccess' => $mastered || $heard >= NSP_MORSE_MASTERY_EXPOSURE_BUDGET || $testAttempts >= NSP_MORSE_MASTERY_ATTEMPT_BUDGET, 'successWithinBudget' => !empty($c['successWithinBudget']), 'exposuresToMastery' => $mastered ? max(0, min(1000000, (int)($c['exposuresToMastery'] ?? 0))) : null, 'attemptsToMastery' => $mastered ? max(0, min(1000000, (int)($c['attemptsToMastery'] ?? 0))) : null, 'msToMastery' => $mastered ? max(0, min(31536000000, (int)($c['msToMastery'] ?? 0))) : null, 'medianResponseMsAtMastery' => $mastered ? max(0, min(600000, (int)($c['medianResponseMsAtMastery'] ?? 0))) : null, 'masteryCondition' => nsp_morse_clean($c['masteryCondition'] ?? ($stats['assignedCondition'] ?? ''), 8), ]; } return $out; } function nsp_morse_safe_payload(array $payload): array { $stats = is_array($payload['stats'] ?? null) ? $payload['stats'] : []; $recent = is_array($stats['recent'] ?? null) ? $stats['recent'] : []; $safeRecent = []; foreach (array_slice($recent, -NSP_MORSE_MAX_RECENT) as $e) { if (is_array($e)) { $safe = nsp_morse_safe_event($e); if ($safe) $safeRecent[] = $safe; } } return [ 'clientVersion' => nsp_morse_clean($payload['clientVersion'] ?? 'hub-morse-v2', 40), 'event' => nsp_morse_clean($payload['event'] ?? '', 40), 'assignedCondition' => nsp_morse_clean($payload['assignedCondition'] ?? '', 8), 'localCreatedAt' => nsp_morse_clean($stats['createdAt'] ?? '', 40), 'localUpdatedAt' => nsp_morse_clean($stats['updatedAt'] ?? '', 40), 'totals' => [ 'totalHeard' => max(0, (int)($stats['totalHeard'] ?? 0)), 'testPrompts' => max(0, (int)($stats['testPrompts'] ?? 0)), 'testAttempts' => max(0, (int)($stats['testAttempts'] ?? 0)), 'testCorrect' => max(0, (int)($stats['testCorrect'] ?? 0)), 'testWrong' => max(0, (int)($stats['testWrong'] ?? 0)), ], 'charSummaries' => nsp_morse_safe_chars($stats), 'recent' => $safeRecent, ]; } function nsp_morse_save_telemetry(): void { $len = (int)($_SERVER['CONTENT_LENGTH'] ?? 0); if ($len > NSP_MORSE_MAX_POST) nsp_json(['ok' => false, 'err' => 'payload too large'], 413); $raw = file_get_contents('php://input'); $payload = json_decode((string)$raw, true); if (!is_array($payload)) nsp_json(['ok' => false, 'err' => 'invalid json'], 400); $visitor = nsp_morse_visitor_hash(); $row = [ 'visitor' => $visitor, 'updatedAt' => gmdate('c'), 'uaHash' => nsp_morse_ua_hash(), 'payload' => nsp_morse_safe_payload($payload), ]; // Explicit: never store raw IP or UA string unset($row['ip'], $row['remote_addr'], $row['user_agent']); $result = nsp_morse_with_rows(function (array $rows) use ($visitor, $row) { $rows[$visitor] = $row; // ONE latest row per hash (non-accumulation) return ['__rows' => $rows, 'count' => count($rows)]; }); nsp_json(['ok' => true, 'visitorHash' => $visitor, 'storedVisitors' => $result['count'] ?? null]); } function nsp_morse_bucket_num(float $value, float $step): string { if ($value <= 0) return 'off'; $rounded = round($value / $step) * $step; return number_format($rounded, $step < 1 ? 1 : 0, '.', ''); } function nsp_morse_median(array $values): ?float { $values = array_values(array_filter($values, 'is_numeric')); $n = count($values); if ($n === 0) return null; sort($values, SORT_NUMERIC); $mid = intdiv($n, 2); return $n % 2 === 1 ? (float)$values[$mid] : (((float)$values[$mid - 1] + (float)$values[$mid]) / 2); } function nsp_morse_aggregate(array $rows): array { $summary = [ 'visitors' => count($rows), 'heard' => 0, 'testAttempts' => 0, 'testCorrect' => 0, 'masteredChars' => 0, 'successWithinBudget' => 0, 'conditions' => [], 'note' => 'Anonymous aggregate by audio profile — learning-speed research, not identity.', ]; foreach ($rows as $row) { $payload = is_array($row['payload'] ?? null) ? $row['payload'] : []; $recent = is_array($payload['recent'] ?? null) ? $payload['recent'] : []; $charSummaries = is_array($payload['charSummaries'] ?? null) ? $payload['charSummaries'] : []; $rowCond = nsp_morse_clean($payload['assignedCondition'] ?? '?', 8) ?: '?'; if (!isset($summary['conditions'][$rowCond])) { $summary['conditions'][$rowCond] = [ 'heard' => 0, 'attempts' => 0, 'correct' => 0, 'masteredChars' => 0, 'successWithinBudget' => 0, '_exposures' => [], ]; } foreach ($recent as $e) { if (!is_array($e)) continue; $snap = is_array($e['snapshot'] ?? null) ? $e['snapshot'] : []; $cond = nsp_morse_clean($snap['assignedCondition'] ?? $rowCond, 8) ?: $rowCond; if (!isset($summary['conditions'][$cond])) { $summary['conditions'][$cond] = [ 'heard' => 0, 'attempts' => 0, 'correct' => 0, 'masteredChars' => 0, 'successWithinBudget' => 0, '_exposures' => [], ]; } if (($e['event'] ?? '') === 'heard') { $summary['heard']++; $summary['conditions'][$cond]['heard']++; } if (($e['event'] ?? '') === 'test-answer') { $summary['testAttempts']++; $summary['conditions'][$cond]['attempts']++; if (!empty($e['correct'])) { $summary['testCorrect']++; $summary['conditions'][$cond]['correct']++; } } } foreach ($charSummaries as $c) { if (!is_array($c)) continue; $cond = nsp_morse_clean($c['masteryCondition'] ?? $rowCond, 8) ?: $rowCond; if (!isset($summary['conditions'][$cond])) { $summary['conditions'][$cond] = [ 'heard' => 0, 'attempts' => 0, 'correct' => 0, 'masteredChars' => 0, 'successWithinBudget' => 0, '_exposures' => [], ]; } if (!empty($c['mastered'])) { $summary['masteredChars']++; $summary['conditions'][$cond]['masteredChars']++; $ex = (int)($c['exposuresToMastery'] ?? 0); if ($ex > 0) $summary['conditions'][$cond]['_exposures'][] = $ex; } if (!empty($c['successWithinBudget'])) { $summary['successWithinBudget']++; $summary['conditions'][$cond]['successWithinBudget']++; } } } $bestSpeed = null; foreach ($summary['conditions'] as $name => &$c) { $c['medianExposuresToMastery'] = nsp_morse_median($c['_exposures']); unset($c['_exposures']); if ($c['masteredChars'] >= 1 && $c['medianExposuresToMastery'] !== null) { if (!$bestSpeed || $c['medianExposuresToMastery'] < $bestSpeed['medianExposuresToMastery']) { $bestSpeed = ['condition' => $name, 'medianExposuresToMastery' => $c['medianExposuresToMastery']]; } } } unset($c); $summary['bestSpeed'] = $bestSpeed; return $summary; } function nsp_morse_summary_api(): void { $agg = nsp_morse_with_rows(function (array $rows) { return nsp_morse_aggregate($rows); }); nsp_json(['ok' => true, 'aggregate' => $agg]); } /** Per-visit Audio Profile A–E biasing field sweep bands (ref-v3). */ function nsp_morse_random_condition(array $summary = []): array { $base = [ ['id' => 'A', 'spinMinHz' => 0.5, 'spinMaxHz' => 1.5, 'spreadMaxHz' => 12, 'lowOscMinHz' => 0.5, 'lowOscMaxHz' => 2.0, 'fieldVolume' => 4, 'movementDepth' => 55], ['id' => 'B', 'spinMinHz' => 1.5, 'spinMaxHz' => 2.5, 'spreadMaxHz' => 16, 'lowOscMinHz' => 1.0, 'lowOscMaxHz' => 3.0, 'fieldVolume' => 5, 'movementDepth' => 65], ['id' => 'C', 'spinMinHz' => 2.5, 'spinMaxHz' => 3.5, 'spreadMaxHz' => 20, 'lowOscMinHz' => 2.0, 'lowOscMaxHz' => 4.0, 'fieldVolume' => 5, 'movementDepth' => 70], ['id' => 'D', 'spinMinHz' => 0.5, 'spinMaxHz' => 4.0, 'spreadMaxHz' => 20, 'lowOscMinHz' => 0.5, 'lowOscMaxHz' => 4.0, 'fieldVolume' => 5, 'movementDepth' => 70], ['id' => 'E', 'spinMinHz' => 3.0, 'spinMaxHz' => 4.0, 'spreadMaxHz' => 20, 'lowOscMinHz' => 3.0, 'lowOscMaxHz' => 4.0, 'fieldVolume' => 6, 'movementDepth' => 75], ]; $best = is_array($summary['bestSpeed'] ?? null) ? (string)($summary['bestSpeed']['condition'] ?? '') : ''; $chosen = null; if ($best !== '' && random_int(1, 100) <= 55) { foreach ($base as $candidate) { if ($candidate['id'] === $best) { $chosen = $candidate; break; } } } if (!$chosen) $chosen = $base[random_int(0, count($base) - 1)]; $chosen['spinMinHz'] = max(0.5, min(4.0, $chosen['spinMinHz'] + random_int(-10, 10) / 100)); $chosen['spinMaxHz'] = max($chosen['spinMinHz'] + 0.2, min(4.0, $chosen['spinMaxHz'] + random_int(-10, 10) / 100)); $chosen['spreadMaxHz'] = max(6, min(24, $chosen['spreadMaxHz'] + random_int(-10, 10) / 10)); $chosen['lowOscMinHz'] = max(0.5, min(4.0, $chosen['lowOscMinHz'] + random_int(-10, 10) / 100)); $chosen['lowOscMaxHz'] = max($chosen['lowOscMinHz'] + 0.2, min(4.0, $chosen['lowOscMaxHz'] + random_int(-10, 10) / 100)); $chosen['fieldVolume'] = max(2, min(10, $chosen['fieldVolume'] + random_int(-1, 1))); $chosen['movementDepth'] = max(35, min(90, $chosen['movementDepth'] + random_int(-5, 5))); $tokenBody = implode('|', [ $chosen['id'], $chosen['spinMinHz'], $chosen['spinMaxHz'], $chosen['spreadMaxHz'], $chosen['lowOscMinHz'], $chosen['lowOscMaxHz'], $chosen['fieldVolume'], $chosen['movementDepth'], gmdate('Y-m-d'), ]); $chosen['token'] = substr(hash_hmac('sha256', $tokenBody, nsp_morse_salt()), 0, 24); $chosen['label'] = 'Audio Profile ' . $chosen['id']; return $chosen; } function nsp_pass_set(string $pass): void { $pass = trim($pass); if (strlen($pass) < 8) nsp_json(['ok' => false, 'err' => 'admin pass min 8 chars'], 400); $h = password_hash($pass, PASSWORD_DEFAULT); if ($h === false) nsp_json(['ok' => false, 'err' => 'hash fail'], 500); file_put_contents(nsp_hash_file(), $h . "\n", LOCK_EX); @chmod(nsp_hash_file(), 0600); nsp_vault_write($pass); } function nsp_pass_is_set(): bool { return is_file(nsp_hash_file()) && trim((string)file_get_contents(nsp_hash_file())) !== ''; } function nsp_ok(?string $pass): bool { if ($pass === null || $pass === '') return false; if (!nsp_pass_is_set()) return false; $h = trim((string)file_get_contents(nsp_hash_file())); return $h !== '' && password_verify(trim($pass), $h); } function nsp_require(): void { // POST body only — never accept admin_pass / seed from query (URL/access logs/Referer). $seed = (string)($_POST['seed'] ?? ''); if ($seed !== '' && nsp_panel_seed_ok($seed)) { return; } $p = (string)($_POST['admin_pass'] ?? ''); if (!nsp_ok($p)) nsp_json(['ok' => false, 'err' => 'admin auth'], 401); } function nsp_handle_admin_api(string $api): bool { if (!str_starts_with($api, 'admin_')) return false; nsp_ensure_site_seed(); if ($api === 'admin_status') { $v = nsp_hub_variant(); // Soft-verify V12: admin_pass_set + site only. No filesystem vault path to strangers. nsp_json(['ok' => true, 'admin_pass_set' => nsp_pass_is_set(), 'site' => $v, 'version' => $v]); } if ($api === 'admin_setup' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { if (nsp_pass_is_set()) nsp_json(['ok' => false, 'err' => 'already set - use admin_login'], 400); // Seed exists at genesis — no unauthenticated land-grab of the legacy password door. $seed = (string)($_POST['seed'] ?? ''); if ($seed === '' || !nsp_panel_seed_ok($seed)) { nsp_json(['ok' => false, 'err' => 'admin auth'], 401); } nsp_pass_set((string)($_POST['admin_pass'] ?? '')); // No absolute vault path on first-setup response (filesystem paths stay off unauth JSON). nsp_json(['ok' => true, 'msg' => 'hash+vault set (seed-proved)']); } if ($api === 'admin_login' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { nsp_require(); $v = nsp_hub_variant(); nsp_json(['ok' => true, 'msg' => 'ok', 'site' => $v, 'vault_hint' => nsp_vault_dir(), 'version' => $v]); } if ($api === 'admin_change_pass' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { nsp_require(); nsp_pass_set((string)($_POST['new_pass'] ?? '')); nsp_json(['ok' => true, 'msg' => 'rotated']); } if ($api === 'admin_get_source' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { nsp_require(); $raw = (string)file_get_contents(__FILE__); nsp_json(['ok' => true, 'bytes' => strlen($raw), 'sha256' => hash('sha256', $raw), 'source' => $raw]); } if ($api === 'admin_put_source' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { nsp_require(); $src = (string)($_POST['source'] ?? ''); if (strlen($src) < 100 || strpos($src, ' false, 'err' => 'bad source'], 400); $bak = __FILE__ . '.bak.' . time(); @copy(__FILE__, $bak); if (file_put_contents(__FILE__, $src, LOCK_EX) === false) nsp_json(['ok' => false, 'err' => 'write failed'], 500); nsp_json(['ok' => true, 'msg' => 'replaced', 'backup' => basename($bak), 'sha256' => hash('sha256', $src)]); } nsp_json(['ok' => false, 'err' => 'unknown admin api'], 404); return true; } function nsp_render_controlpanel(): void { header('Content-Type: text/html; charset=UTF-8'); header('Cache-Control: no-store'); $v = nsp_hub_variant(); $site = nsp_hub_label() . ' MAP · AB hub'; $h = htmlspecialchars($site); echo ''; echo '' . $h . ' Control
'; echo '

DNA · MAP · AB hub · pinwheel · ' . htmlspecialchars($v) . '

'; echo '

' . $h . ' · Control Panel

'; echo '

Site-local renter key for THIS domain only (com/org/net AB triad shares UI code, not vaults). ' . 'Paste this host\'s site wallet seed (vault SITE_WALLET_SEED.txt / data/site.seed). ' . 'UTTER control of THIS index.php. Not OS root. Independent vault per host. No visitor NSU on hub. ' . 'Going-forward door is site seed; legacy admin password (if already set) is one-release migrate read only.

'; echo '
'; echo ''; echo ''; echo ''; echo '
'; echo ''; echo '

Owner: seed auto-generated at first boot into data/site.seed + vault SITE_WALLET_SEED.txt (root pull). Paste seed → unlock. No password product path. No recovery desk. Hub is not a mint. Not a visitor account.

'; echo '

Vault: -

'; echo ''; echo '

Source (utter control)

'; echo '
'; echo '

← back

'; exit; } /** * In-file skill sketches: Morse (v1) + ASL text-only peer deck (same practice/test shell). * Two skills only — text letter hints for ASL, tones for Morse. No camera/video; no third skill. */ /** * In-file skill sketches: Morse (v2 competency engine) + ASL text-only peer deck. * Morse: clean 550 Hz tone SEPARATE from optional stereo field (profiles A–E). * Two skills only. No camera/video; no third skill. Hub DNA: free · no signup. */ function nsp_render_skill_sketch(string $skill): void { $skill = $skill === 'asl' ? 'asl' : 'morse'; $v = nsp_hub_variant(); $title = $skill === 'asl' ? 'ASL · skill deck' : 'Morse · competency sketch'; header('Content-Type: text/html; charset=UTF-8'); header('Cache-Control: no-store'); $hubHome = './'; $morseJson = json_encode([ 'A'=>'.-','B'=>'-...','C'=>'-.-.','D'=>'-..','E'=>'.','F'=>'..-.','G'=>'--.','H'=>'....', 'I'=>'..','J'=>'.---','K'=>'-.-','L'=>'.-..','M'=>'--','N'=>'-.','O'=>'---','P'=>'.--.', 'Q'=>'--.-','R'=>'.-.','S'=>'...','T'=>'-','U'=>'..-','V'=>'...-','W'=>'.--','X'=>'-..-', 'Y'=>'-.--','Z'=>'--..','0'=>'-----','1'=>'.----','2'=>'..---','3'=>'...--','4'=>'....-', '5'=>'.....','6'=>'-....','7'=>'--...','8'=>'---..','9'=>'----.', ], JSON_UNESCAPED_SLASHES); $aslJson = json_encode([ 'A'=>'fist thumb side','B'=>'flat hand thumb in','C'=>'C shape','D'=>'index up rest touch', 'E'=>'fist fingertips','F'=>'OK circle three up','G'=>'index+thumb side','H'=>'two fingers side', 'I'=>'pinky up','J'=>'pinky draw J','K'=>'index mid V thumb','L'=>'L shape', 'M'=>'three over thumb','N'=>'two over thumb','O'=>'O circle','P'=>'K pointing down', 'Q'=>'G pointing down','R'=>'cross index mid','S'=>'fist thumb front','T'=>'thumb under index', 'U'=>'two fingers up','V'=>'peace V','W'=>'three fingers','X'=>'hook index', 'Y'=>'thumb+pinky','Z'=>'index draw Z', ], JSON_UNESCAPED_SLASHES); $spokenJson = json_encode([ 'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G','H'=>'H','I'=>'I','J'=>'J', 'K'=>'K','L'=>'L','M'=>'M','N'=>'N','O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T', 'U'=>'U','V'=>'V','W'=>'W','X'=>'X','Y'=>'Y','Z'=>'Z', '0'=>'zero','1'=>'one','2'=>'two','3'=>'three','4'=>'four','5'=>'five','6'=>'six','7'=>'seven','8'=>'eight','9'=>'nine', ], JSON_UNESCAPED_SLASHES); $serverBootstrap = null; if ($skill === 'morse') { /* Fail-open: soft row lock so unwritable data/ still serves the trainer. */ $got = nsp_morse_with_rows(static function (array $rows) { return nsp_morse_aggregate($rows); }, false); $agg = is_array($got) ? $got : ['visitors' => 0, 'bestSpeed' => null]; $cond = nsp_morse_random_condition($agg); $serverBootstrap = [ 'appVersion' => 'hub-morse-v2', 'condition' => $cond, 'aggregate' => [ 'visitors' => (int)($agg['visitors'] ?? 0), 'bestSpeed' => $agg['bestSpeed'] ?? null, ], 'mastery' => [ 'window' => NSP_MORSE_MASTERY_WINDOW, 'required' => NSP_MORSE_MASTERY_REQUIRED, 'maxMedianMs' => NSP_MORSE_MASTERY_MAX_MEDIAN_MS, 'exposureBudget' => NSP_MORSE_MASTERY_EXPOSURE_BUDGET, 'attemptBudget' => NSP_MORSE_MASTERY_ATTEMPT_BUDGET, ], ]; } echo ''; echo ''; echo '' . htmlspecialchars($title) . '
'; echo '
← MAPsketch · ' . htmlspecialchars($skill) . ($skill === 'morse' ? ' · v2' : '') . ''; echo '' . ($skill === 'morse' ? 'ASL →' : 'Morse →') . '
'; echo '
·
'; echo '
·
'; echo '
'; echo '
' . ($skill === 'morse' ? 'Clean Morse tone · optional stereo field · practice or test' : 'Pick a stream · START to practice') . '
'; echo '
idle · no account · browser-local mastery
'; if ($skill === 'morse') { echo '
Audio Profile
'; } echo '
'; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ($skill === 'asl') { echo ''; } else { echo ''; } echo ''; echo '
'; if ($skill === 'morse') { echo ''; echo '
Optional stereo field (headphones) · separate from Morse tone'; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo '
'; echo '
heard 0tests 0mastered 0agg visitors 0
'; echo '
'; echo '

Morse tone is clean · centered ~550 Hz · field is optional experiment · headphones help for binaural

'; echo '

Anonymous organism hook: one latest hashed row per visitor (HMAC of network+agent, never raw IP). Improves defaults by learning speed, not raw accuracy. No account · no recovery · no identity desk.

'; } else { echo '

Two skills only · ASL text hints · free · no signup · no camera

'; } echo '
'; } if (isset($_GET['src']) || isset($_GET['download']) || (isset($_GET['api']) && $_GET['api'] === 'src')) { $raw = (string)file_get_contents(__FILE__); header('Content-Type: text/plain; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('X-NS-Sha256: ' . hash('sha256', $raw)); if (isset($_GET['download'])) { header('Content-Disposition: attachment; filename="nosignup-' . nsp_hub_variant() . '.php"'); } echo $raw; exit; } /* Site seed genesis BEFORE any admin API / controlpanel (no land-grab window). */ $api = (string)($_GET['api'] ?? $_POST['api'] ?? ''); /* MORSE-v2: organism research hooks (hashed anon; no accounts) */ if ($api === 'telemetry' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { nsp_morse_save_telemetry(); } if ($api === 'summary') { nsp_morse_summary_api(); } /* WIRING-PARITY: public selfhash (trade-shape; no auth) */ if ($api === 'selfhash') { nsp_json([ 'ok' => true, 'version' => 'hub-sketch-1', 'sha256' => @hash_file('sha256', __FILE__) ?: null, 'file' => basename(__FILE__), ]); } $wantPanel = isset($_GET['controlpanel']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/controlpanel/?(\?|$)#', (string)$_SERVER['REQUEST_URI'])); if (($api !== '' && str_starts_with($api, 'admin_')) || ($wantPanel && $api === '')) { nsp_ensure_site_seed(); } if ($api !== '' && str_starts_with($api, 'admin_')) { nsp_handle_admin_api($api); } if ($wantPanel && $api === '') { nsp_render_controlpanel(); } /* In-file sketch / custom-site memory — served from this PHP */ $sketch = strtolower(preg_replace('/[^a-z0-9_-]/', '', (string)($_GET['sketch'] ?? '')) ?? ''); if ($sketch === 'morse' || $sketch === 'asl' || $sketch === 'train') { nsp_render_skill_sketch($sketch === 'train' ? 'morse' : $sketch); exit; } $nsHubVar = nsp_hub_variant(); $nsHubTitle = nsp_hub_label(); $nsHubPoolJson = json_encode(nsp_hub_random_pool(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?> <?= htmlspecialchars($nsHubTitle) ?> · MAP sketchpad hub