'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 '
Site wallet seed (12 words) ';
echo '
';
echo '
Unlock with site seed ';
echo '
';
echo '
Legacy password (one-release migrate only) ';
echo 'Only if data/admin.pass.hash already exists from an older deploy. Not the product path.
';
echo 'Legacy admin password ';
echo ' ';
echo 'Unlock with legacy pass ';
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 '
Migrate: rotate legacy password (optional) ';
echo 'New password Update
';
echo '
Source (utter control) Load Save ';
echo '
index.php source ';
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 '';
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 '
';
}
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
Options
Mirrors
Donate
MAP · sketchpad hub · AB landing (= htmlspecialchars($nsHubVar) ?>)
= htmlspecialchars($nsHubTitle) ?>
Map Spokes = NSU sites · center ? = random product or sketch Sketchpad Morse & ASL trainers live here — equal real-estate No signup Explore free · no account · no app install Tools Options · Mirrors · Donate work before ENTER
Experimental · unmoderated · no recovery desk enter at your own risk.
I AGREE TO THESE TERMS ENTER
I am a minor — leave
x Mirrors Free tributaries. Host a copy · help the network · zero cut · no price power. Mirrors welcome — gossip and findability. No daily faucet wage in v1 until hits are parent-verifiable. Host because the swarm is hard to kill. One .php file — drop free on any PHP host. Cashflow / NSU rails stay on nosignup.trade (not minted here · hub MAP has no visitor faucet).
↓ Download this file · View source
Known public roots:
Close
x Donate One .php file. No build step. Optional — never required.
BITCOIN bc1qqnu6n0jztxl4f6krv7klradghle09uhyu7uymz
MONERO 8Ab24DppUvcdtHfm7K8gTqdBTmPCBiak1GwxgPm1C3osYVQL2QdC1C8GMwggKF77RKKzDgP2R8E3VH8ifetsKms5AqkVyVg
LITECOIN ltc1qlpdy8qzejcmjdn6vwarpyz8djdlk780w4qkwyp
Tap address to copy · also see nosignup.trade faucet/rails
Close