'morse', 'kind' => 'sketch', 'label' => 'Morse (binaural trainer)', 'href' => '?sketch=morse'],
['id' => 'asl', 'kind' => 'sketch', 'label' => 'ASL (text skill deck)', 'href' => '?sketch=asl'],
// Add more custom sites here: ['id'=>'x','kind'=>'sketch','label'=>'…','href'=>'?sketch=x'],
];
}
/** Empire sister product sites always in the ? pool (equal weight with sketches). */
function nsp_product_pool(): array {
return [
['id' => 'trade', 'kind' => 'url', 'label' => 'Trade (money)', 'href' => '//nosignup.trade/'],
['id' => 'chat', 'kind' => 'url', 'label' => 'Chat', 'href' => '//nosignup.chat/'],
['id' => 'work', 'kind' => 'url', 'label' => 'Work', 'href' => '//nosignup.work/'],
['id' => 'market', 'kind' => 'url', 'label' => 'Market', 'href' => '//nosignup.market/'],
['id' => 'date', 'kind' => 'url', 'label' => 'Date (personals)', 'href' => '//nosignup.date/'],
['id' => 'fun', 'kind' => 'url', 'label' => 'Fun', 'href' => '//nosignup.fun/'],
['id' => 'info', 'kind' => 'url', 'label' => 'Info', 'href' => '//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 wallet).
*/
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 ledger or mint on this crop. Money/buy: nosignup.trade. 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 {
nsp_pass_burn();
$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_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;
}
/** Drop Morse visitor_stats rows whose updatedAt is older than 32 days. Hashed id only. */
function nsp_morse_prune_stale_rows(array $rows): array {
$cutoff = time() - (32 * 86400);
$kept = [];
foreach ($rows as $id => $row) {
if (!is_array($row)) continue;
$at = (string)($row['updatedAt'] ?? '');
$ts = $at !== '' ? strtotime($at) : false;
if (!is_int($ts) || $ts < $cutoff) continue;
$kept[$id] = $row;
}
return $kept;
}
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, nsp_morse_prune_stale_rows($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);
/* RR361 P1-HUB-TELEMETRY-JSON-PLAIN: visitor plain English (not bare invalid json / payload too large) */
if ($len > NSP_MORSE_MAX_POST) nsp_json(['ok' => false, 'err' => 'Telemetry body is too large — send a smaller JSON update'], 413);
$raw = file_get_contents('php://input');
$payload = json_decode((string)$raw, true);
if (!is_array($payload)) nsp_json(['ok' => false, 'err' => 'Telemetry body must be JSON (object)'], 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 {
$f = __DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'morse' . DIRECTORY_SEPARATOR . 'visitor_stats.jsonl';
if (!is_file($f)) {
nsp_json(['ok' => true, 'aggregate' => nsp_morse_aggregate([])]);
return;
}
$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)));
$chosen['token'] = bin2hex(random_bytes(12));
$chosen['label'] = 'Audio Profile ' . $chosen['id'];
return $chosen;
}
function nsp_pass_burn(): void {
$paths = [
nsp_hash_file(),
nsp_data_dir() . DIRECTORY_SEPARATOR . 'admin.pass.txt',
nsp_vault_dir() . DIRECTORY_SEPARATOR . 'ADMIN_PASSWORD.txt',
];
foreach ($paths as $p) {
if (is_string($p) && $p !== '' && is_file($p) && !is_link($p)) {
@unlink($p);
}
}
}
function nsp_require(): void {
// POST body only — never accept seed from query (URL/access logs/Referer).
nsp_pass_burn();
$seed = (string)($_POST['seed'] ?? '');
if ($seed !== '' && nsp_panel_seed_ok($seed)) {
return;
}
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: site + version only. No filesystem vault path to strangers.
nsp_json(['ok' => true, 'site' => $v, 'version' => $v]);
}
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]);
}
/* LORD SOVEREIGNTY — rotate this crop's panel seed.
*
* Until this existed a lord could not become independent of the king. The
* ten panel seeds are minted by genesis and every one of them is printed in
* the king's GENESIS-INFO.txt, so the key to a rented crop was issued by the
* landlord and the landlord kept a copy. admin_change_pass rotates only the
* legacy PASSWORD, which changes nothing: nsp_require() accepts the seed
* directly, so the seed is the real door and it could never be changed.
*
* That is fine for a staff position and wrong for a tenancy - and the rent
* system (admin_rent_claim, operator_addr, NST_RENT_NSU_PER_DAY) says
* tenancy is the intent. A tenant whose landlord holds a key to the safe is
* not a tenant.
*
* CONFIRMATION IS REQUIRED AND CASE-SENSITIVE, matching the WIPE prompt in
* Deploy.bat. Re-keying is irreversible with no recovery desk, so it must be
* un-runnable by accident rather than merely documented as dangerous.
*
* THE ADDRESS CHANGES, AND THAT HAS CONSEQUENCES THE CALLER MUST SEE.
* Addresses derive from seeds, so a new seed is a new wallet:
* - the crop's existing NSU stays at the OLD address, which the old seed
* still opens; move it deliberately, it is not swept
* - trade pays this crop's emission share to the address in its on-chain
* crop registry, which still names the OLD one until a successor is
* anchored there
* Both are reported in the response rather than left to be discovered.
*/
if ($api === 'admin_rekey' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
nsp_require();
if ((string)($_POST['confirm'] ?? '') !== 'REKEY') {
nsp_json([
'ok' => false,
'err' => 'Re-keying replaces this crop\'s panel seed permanently. There is no '
. 'recovery desk. POST confirm=REKEY to proceed.',
'confirm_required' => 'REKEY',
], 400);
}
$oldSeed = nsp_norm_seed((string)@file_get_contents(nsp_seed_file()));
$oldAddr = $oldSeed !== '' ? nsp_addr_from_seed($oldSeed) : '';
$new = nsp_generate_site_seed();
$newAddr = nsp_addr_from_seed($new);
if ($new === '' || $newAddr === '' || $newAddr === $oldAddr) {
nsp_json(['ok' => false, 'err' => 'seed generation failed'], 500);
}
/* Vault note first: it is the operator's offline copy, so if the second
* write fails the seed still exists somewhere other than this response.
* The old seed keeps working until site.seed itself is replaced, so a
* half-finished rotation locks nobody out. */
nsp_vault_site_seed_note($new);
if (@file_put_contents(nsp_seed_file(), $new . "\n", LOCK_EX) === false) {
nsp_json(['ok' => false, 'err' => 'could not write site.seed - crop unchanged, old seed still valid'], 500);
}
@chmod(nsp_seed_file(), 0600);
/* Read back before claiming success. Reporting a rotation that did not
* land would strand the lord with a seed the crop does not accept. */
$check = nsp_norm_seed((string)@file_get_contents(nsp_seed_file()));
if ($check !== nsp_norm_seed($new)) {
nsp_json(['ok' => false, 'err' => 'readback mismatch - rotation not confirmed'], 500);
}
nsp_json([
'ok' => true,
'seed' => $new,
'seed_shown_once' => true,
'old_addr' => $oldAddr,
'new_addr' => $newAddr,
'vault' => 'SITE_WALLET_SEED.txt',
'next_steps' => [
'SAVE THIS SEED OFFLINE NOW. It is shown once and there is no recovery desk.',
'The king\'s copy of the previous seed no longer opens this crop.',
'Your NSU is still at the OLD address - the old seed opens that wallet. '
. 'Transfer it to the new address deliberately; nothing is swept for you.',
'Trade still pays this crop\'s emission to the OLD address until a successor '
. 'is anchored in its on-chain crop registry.',
],
'msg' => 'Panel seed rotated. This crop is now yours alone.',
]);
}
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 wallet on hub — money/buy on nosignup.trade. '
. 'Panel door is site seed only. Leftover admin password files are unlinked.
';
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.
';
if ($skill === 'morse') {
echo '';
/* Panel ships OPEN. The operator trainer (ref v2.1.0 / v3.0.0-nsu) showed the whole
config grid on screen; collapsing it behind a closed is what made the
sketch read as a stripped-down trainer even though the engine is complete. */
echo 'Optional stereo field (headphones) · separate from Morse tone';
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). Optional field telemetry — may stay idle until enough practice rows land; never claims live mastery it does not have. 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) */
/** P1-HUB-TELEMETRY-METHOD RR210: non-POST must not fall through as MAP HTML (200).
* Sisters chat/market/work/date answer write routes with 405 POST only. */
if ($api === 'telemetry' && strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? '')) !== 'POST') {
nsp_json(['ok' => false, 'err' => 'POST only'], 405);
}
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(); }
/* RR325 P1-HUB-UNKNOWN-API-PLAIN: unknown ?api= must not fall through as MAP HTML 200 */
if ($api !== '' && !str_starts_with($api, 'admin_') && $api !== 'telemetry' && $api !== 'summary' && $api !== 'selfhash') {
nsp_json([
'ok' => false,
'err' => 'Unknown MAP API — use selfhash, summary, telemetry (POST), or open the face pinwheel',
'api' => $api,
'money' => 'nosignup.trade',
], 404);
}
/* EMPIRE SETUP GATE: while trade setup unfilled, public faces → trade console */
if ($api === '' && !isset($_GET['src']) && !isset($_GET['download']) && !isset($_GET['controlpanel']) && !isset($_GET['empire_setup']) && !isset($_GET['setup']) && !isset($_GET['sketch'])) {
$uri = (string)($_SERVER['REQUEST_URI'] ?? '');
if (!preg_match('#/(controlpanel|setup)/?(\?|$)#', $uri)) {
$dataDir = __DIR__ . DIRECTORY_SEPARATOR . 'data';
$local = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup.json';
$ok = false;
if (is_file($local)) {
$lj = json_decode((string)@file_get_contents($local), true);
$ok = is_array($lj) && !empty($lj['filled']);
}
if (!$ok) {
$cache = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup_status_cache.json';
$now = time();
if (is_file($cache)) {
$c = json_decode((string)@file_get_contents($cache), true);
if (is_array($c) && isset($c['ts'], $c['filled']) && ($now - (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';
$raw = @file_get_contents($scheme . '://nosignup.trade/?api=empire_setup_status', false, $ctx);
$httpsHit = false;
if (is_string($raw) && $raw !== '') {
$j = json_decode($raw, true);
if (is_array($j) && array_key_exists('filled', $j)) {
$httpsHit = true;
if (!empty($j['filled'])) {
$ok = true;
}
if (is_dir($dataDir)) {
@file_put_contents($cache, json_encode(['ts' => time(), 'filled' => !empty($j['filled'])]) . "\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;
}
}
}
/* 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) ?> · free map of nosignup sites
Free tributaries. Host a copy · help the network · zero cut · no price power. Mirrors welcome — gossip and findability. Hosting is unpaid until parent-proven hits (no free daily host pay); host so the swarm stays hard to kill. One .php file — drop free on any PHP host. Money/buy stays on nosignup.trade (not minted here · this map has no visitor wallet).