* Edits wear the ends of a long file first. Both caps carry the same four * parts, so each end can rebuild the other: if one cap is lost or damaged, * copy the surviving cap over it verbatim. The truth kernel survives edits, * and the function hashes let anyone prove the economy and identity * helpers are still byte-identical. Existing DO NOT DELETE blocks, the * charter and history prose stay in place and are revised accurately. Latest * explicit owner decisions govern conflicts; old wording is not an automatic rollback rule. * * * FRONT CAP: charter, face purpose, truth kernel * CHARTER (summary of FINAL-PACK MAINTENANCE CHARTER, NOSIGNUP.DATE * CONTRACT, FALLBACK and the ECONOMY+IDENTITY tail): * - One stock-PHP file; no composer/npm/database dependency. No visitor * signup, recovery account, tracking profile, shared bank, SSO or paywall * on basic use. ?src=1 and ?download=1 stay auditable. * - This host owns only its own data, vault and operator panel. A panel is * never a visitor identity. Public status never discloses vault or * filesystem detail; admin passwords go in POST bodies, never URLs. * - NEVER BECOME: opaque matching authority, people score or rank, * engagement-ranked feed, icebreaker generator, identity verification, * boosts, unblur or pay-to-rise, independent wallet ledger, account * system, fake profiles, or dating-authority copy. * - Advisory text never becomes a social decision. Keep the narrow hard * safety rules narrow; a soft flag fails open. * - Fallback: the HTML face always serves. No disk: no profile/media * write. No RAM: rate limiting fails open. * FACE PURPOSE: nosignup.date is the no-signup 18+ dating face, an * ephemeral adult personals and meet board ("Meet people nearby. Keep in * touch."). Four tabs: Profile, Map, Keeps (chat: Who-Kept-you list and * pair threads), Browse. Every visit carries the 18+ risk and consent gate. * Cards show Contact (or an honest missing line); an empty board shows an * honest Create-your-card CTA. Keeps are directional: keeping someone lets * them message you, and replying keeps them back. There is no mandatory * mutual match, and a Keep is not proof of mutual interest. * Identity colour: pink/magenta #cc22aa (--ns-face-accent). * TRUTH KERNEL: * 1. DATE forgets. Profiles, photos, Keeps, pair threads, owner indexes and * DATE-only browser keys follow one 30-day ceiling (NSD_EXPIRY_DAYS 30). * Reads never renew. A Keep or a successful thread send renews only the * sender's own card. * 2. Date forgetting must not erase the shared wallet: nosignup_wallet_seed, * nosignup_device_id and economy telomere keys are not DATE state. * 3. Trade owns the only chain (nsu_trade_origin). This face has no visitor * ledger, treasury, mint, faucet, rent claim, tax or operator_addr. NSU * here is a donation. Organic people and listing rank is not for sale. * 4. A ticket identifies a browser-held instance, not a verified person. * Never derive, recover or merge a wallet from IP or fingerprints. * 5. Financial, paid and emission gates stay closed until Sebastian * releases them. * * * BACK CAP: economy+identity function inventory * Source: the nine helpers named in the STRUCTURE / ECONOMY+IDENTITY * TELOMERE block at the file tail, plus its IDENTITY slot pointer * nsp_addr_from_seed (vault cluster, LEFT IN PLACE). Each is declared * exactly once in this file. This list never redeclares any of them. * Hash method: token_get_all() over the whole file. Body = the exact source * bytes from the `function` keyword of `function NAME(` through its matching * closing brace (depth counted on '{', T_CURLY_OPEN and * T_DOLLAR_OPEN_CURLY_BRACES against '}'). Value = sha256 hex of those * bytes, with no added newline. Tool: tools/funchash.php in the deploy * workspace. * - nsu_crop_tld(): string -- this crop's TLD from HTTP_HOST * (nosignup.); '' when unknown so callers fail closed. * sha256 ed37cfe99999b98e615c3d3feba0bce832fb39b3a0f21bd24898685db41e075f (254 bytes) * - nsu_trade_origin(): string -- returns 'https://nosignup.trade'; Trade * owns the only chain, every crop asks it. * sha256 0adf9d607f0e94dcb301f5dbb0249215c11fca545e705665b52cbf645fee6db7 (76 bytes) * - nsu_ad_sites(): array -- the ten empire crop TLDs that may carry an ad * board. * sha256 eace55daa6d916e0fcd185a2394eae65dceb4785b1881e90890522f2c6603c73 (117 bytes) * - nsu_ad_roll_n(): int -- random ad cell number, random_int(0, 9999). * sha256 14442da5dc0d35f8213e553ba5111657efc14b5ea206e5cac3506d9518c5babc (61 bytes) * - nsu_ad_slot_url(?site, ?n): string -- Trade ad-slot URL for site * (default this crop), n clamped 0..9999; '' for an unknown crop. * sha256 d61b5b7e97342ecc0b6ed154b6f20909fc45e61f08fa9edd1b92bd170958421a (390 bytes) * - nsu_board_endpoint(): string -- nsu_ad_slot_url() for this crop. * sha256 6143293c604bf7346eca3a52615f19dbfcd29249747d270c4968d5ea219467aa (67 bytes) * - nsu_ad_slot_html(extraClass): string -- escaped 49-cell ad aside for * this crop; '' for an unknown crop; grants no ownership. * sha256 b297f03ac3e33c6364246aacba5328c6187a6f6686704bbf6728dc944bc797bc (705 bytes) * - nsu_report_endpoint(): string -- Trade ad_report URL for this crop or * ''; grants no economic authority. * sha256 01cf2e209f80d6a9de55a4492e4fcc3b4033f34b6ca9632e06f81320b6b47e5a (209 bytes) * - ns_identity_badge_later(): string -- badge stub, returns ''; no second * mill on this face. * sha256 bb543f5be7b2785ed1f70c31d6db9e21959b5b8ad43ba888375eac0700f0a1b5 (57 bytes) * - nsp_addr_from_seed(seed): string -- owner-panel address * sha256('nsu-addr-v1|' . normalized seed); identity slot, not a visitor * wallet. * sha256 15f02a224094227539cb792006fc2167a192b5e9739ca8f6ade8b249c05719ee (119 bytes) * * * * - Strict improvement only. Each change must leave every page working at * least as well as before and pass its proofs, or it is reverted. * - Alternate waves: a wave of improvements, then a wave of * simplifications with no behaviour change, then repeat. * - Economy first. Check the function hashes above before and after every * wave. A hash change needs an owner-approved reason and an updated cap * at both ends. * - 30-day default expiry stays: NSD_EXPIRY_DAYS 30, reads never renew, * and a Keep or a successful thread send renews only the sender's own * card. * - Trade owns the only chain. NSU is a donation. No second ledger, mint, * faucet or tax on this face. * - Gates stay closed until Sebastian releases them. * - Keep both caps identical. If one end changes, copy it to the other. * * ===================================================================== * * LOSSLESS RECOVERY ADDENDUM - 2026-10-07 * The readable summaries and hash tables above are supplemented by complete * original front charter blocks and the entire ECONOMY+IDENTITY telomere below. * Each part preserves its exact bytes, line endings, length and SHA-256. * Encoding keeps comment terminators and PHP declarations inert. No executable * declaration is duplicated. Either end alone contains all recovery parts. * Decode the base64 envelope as JSON, then each part.data as base64; verify * part.bytes and part.sha256 before recovery. Never execute the decoded text. * Pack tool: NSU-DNA/tools/verify-pins.php --telomeres-only PACK_DIRECTORY. * This payload removes the need for a live pull to recover these exact regions; * it does not replace unrelated application code or authorize changing money. * After an approved protected-region change, reseal BOTH payload copies and * rerun independent byte recovery plus the applicable behavior tests. * NSU-RECOVERY-V1-BEGIN * eyJzY2hlbWEiOiJuc3UtdGVsb21lcmUtcmVjb3ZlcnkvMSIsInNvdXJjZV9zaGEyNTYiOiJhNjNjZWUzODVlZDY3N2FmMDU1NzNh * NjNjNTc2ZWNmNDg4OTg1NmU2YmY5Y2JiNTZlYWYzNmY3OTkwZjQ2NjI0IiwicGFydHMiOlt7ImlkIjoidGVsb21lcmUxLTAxIiwi * bGFiZWwiOiJGSU5BTC1QQUNLIE1BSU5URU5BTkNFIENIQVJURVIgLS0gRE8gTk9UIERFTEVURS4iLCJieXRlcyI6MzcxMSwic2hh * MjU2IjoiMDJhOGM3N2E5ZTA5NWYwODY1Yzc1MjE4NGVmNmQyNzkwOGI4MGU1OWQ0NmUwMGUyMjQ4MjI3NTIwZTQ2MjIwZSIsImRh * dGEiOiJMeW9xQ2lBcUlFTlZVbEpGVGxRZ1VrVldTVk5KVDA0Z01qQXlOaTB4TUMweE15QXZJRkl3TlRvZ1MyVmxjQ0IwYUdseklH * SnNiMk5ySUdsdUlIQnNZV05sTGlCVWFHVWdjR0ZwY21Wa0NpQXFJR05oY0hNZ1oyOTJaWEp1SUdGeVkyaHBkR1ZqZEhWeVpTOVZT * U0JoYm1RZ2JXbHVhVzExYlNCdVpXTmxjM05oY25rZ1kyaGhibWRsTGlCUWNtOTJaU0IwYUdVZ1pHVm1aV04wT3dvZ0tpQndjbVZ6 * WlhKMlpTQmpiM0p5WldOMElHSmxhR0YyYVc5eU95QmhaR1FnYm04Z2MzQmxZM1ZzWVhScGRtVWdjM1JoZEdVc0lIUnBiV1Z5SUc5 * eUlHTnZiblJ5YjJ3dUNpQXFJRkpsZG1selpTQnRkWFJoWW14bElHUmxkR0ZwYkhNZ2QybDBhQ0JsZG1sa1pXNWpaU3dnYm05MElH * UmxiR1YwYVc5dUlHOXlJR0pzYVc1a0lISmxkbVZ5YzJsdmJpNEtJQ29nUzJWbGNDQjBhR2x6SUhOcGRHVW5jeUIzYjNKckwyVjJh * V1JsYm1ObElHbHpiMnhoZEdWa0lHRnVaQ0JpYjNWdVpDQjBieUIwYUdVZ2RHVnpkR1ZrSUhOdmRYSmpaU0JvWVhOb0xnb2dLZ29n * S2lCR1NVNUJUQzFRUVVOTElFMUJTVTVVUlU1QlRrTkZJRU5JUVZKVVJWSWdMUzBnUkU4Z1RrOVVJRVJGVEVWVVJTNEtJQ29LSUNv * Z1ExVlNVa1ZPVkNCUVVrOUVWVU5VSUVGTlJVNUVUVVZPVkNBeU1ESTJMVEV3TFRBeklDMGdiM2R1WlhJdFpHbHlaV04wWldRZ1pX * NTBjbUZ1WTJVZ2NtOTFibVF1Q2lBcUlGQjFjbkJ2YzJVNklFMWxaWFFnY0dWdmNHeGxJRzVsWVhKaWVTNGdTMlZsY0NCcGJpQjBi * M1ZqYUM0S0lDb2dWWE5sSUc5dVpTQnlaV0ZrWVdKc1pTQndjbTl3YjNKMGFXOXVZV3dnWlc1MGNtRnVZMlVnWm1GdGFXeDVMQ0Jq * WVhKeWVXbHVaeUIwYUdseklHWmhZMlVuY3dvZ0tpQnZkMjRnYlc5MGFXWWdZVzVrSUhCaGJHVjBkR1V1SUV4bFlXUWdkMmwwYUNC * MWMyVm1kV3dnY0d4aGFXNGdkMjl5WkhNN0lIQnlaWE5sY25abElIUm9aUW9nS2lCdFpXRnVhVzVuSUc5bUlHTnZibk5sYm5Rc0lI * SnBjMnNnWVc1a0lHRmpkSFZoYkNCallYQmhZbWxzYVhSNUlHeHBiV2wwY3k0Z1JtbDBJRzV2Y20xaGJBb2dLaUIyYVdWM2N5QjNh * WFJvYjNWMElITmpjbTlzYkdKaGNuTTdJSEpsZEdGcGJpQmhiaUJoWTJObGMzTnBZbXhsSUhwdmIyMHZjbVZtYkc5M0lHWmhiR3hp * WVdOckxnb2dLaUJUYUdGeVpXUWdkMkZzYkdWMElHRmpZMlZ6Y3lCcGN5QnViM1FnWVNCdVpYY2dabUZqWlNCc1pXUm5aWElzSUcx * cGJuUWdiM0lnWVdOamIzVnVkQzRLSUNvZ1FXUjFiSFFnY0dWeWMyOXVZV3h6SUdOdmJXSnBibVVnVUhKdlptbHNaU3dnVFdGd0xD * QkxaV1Z3Y3lCaGJtUWdRbkp2ZDNObElIZHBkR2h2ZFhRZ2MybG5iblZ3TGdvZ0tpQkxaV1Z3SUdkeVlXNTBjeUIwYUdVZ2IzUm9a * WElnY0dWeWMyOXVJSEJsY20xcGMzTnBiMjRnZEc4Z2JXVnpjMkZuWlNCNWIzVTdJR2wwSUdseklHNXZkQW9nS2lCd2NtOXZaaUJ2 * WmlCdGRYUjFZV3dnYVc1MFpYSmxjM1F1SUZOMWNIQnZjblFnWTJ4bFlYSXNJSGR2Y210cGJtY2dZMjl1YzJWdWRDQmpiMjUwY205 * c2N5NEtJQ29nVkdobElHVjRaV04xZEdGaWJHVWdabTl5WjJWMGRHbHVaeUJ3YjJ4cFkza2dhWE1nTXpBZ1pHRjVjenNnY0dGemMy * bDJaU0J5WldGa2N5QmtieUJ1YjNRS0lDb2djbVZ1WlhjZ2FYUXVJRVJCVkVVZ2MzUmhkR1VnWVc1a0lITm9ZWEpsWkNCM1lXeHNa * WFFnWVdOalpYTnpJSEpsYldGcGJpQnpaWEJoY21GMFpTNEtJQ29nVTI5MWNtTmxJR0YxZEdodmNtbDBlVG9nZG1WeWFXWnBaV1Fn * WTNWeWNtVnVkQ0J5YjI5MElEZ3VjR2h3TGdvZ0tpQlBiR1FnWTJWc2JITXZjM2x1WXkxd1lXTnJJSEJ2YVc1MFpYSnpJR0Z5WlNC * b2FYTjBiM0pwWTJGc095QjBhRzl6WlNCalpXeHNjeUJoY21VZ1lXSnpaVzUwTGdvZ0tpQlNaV052Ym1OcGJHVWdiR2wyWlM5eVlY * Y3ZjMjkxY21ObElHSjVkR1Z6SUdKbFptOXlaU0IzY21sMGFXNW5PeUJrYnlCdWIzUWdjbVYyYVhabElHOXNaQ0J3WVdOcmN5NEtJ * Q29nVkdocGN5QmhiV1Z1WkcxbGJuUWdjM1Z3WlhKelpXUmxjeUJqYjI1bWJHbGpkR2x1WnlCdmJHUWdiR0Y1YjNWMEwzSnZZV1J0 * WVhBZ2NISnZjMlVnYjI1c2VTNEtJQ29nVUhKbGMyVnlkbVZrSUdocGMzUnZjbmtnYVhNZ2JtOTBJR0VnZEdGemF5QnhkV1YxWlNC * dmNpQndjbTl2WmlCdlppQnBiWEJzWlcxbGJuUmxaQ0IzYjNKckxnb2dLZ29nS2lCVWFHbHpJR052YlcxbGJuUWdjSEpsYzJWeWRt * VnpJSFJvWlNCd2NtOWtkV04wSUdKdmRXNWtZWEo1SUhkb1pXNGdkR2hwY3lCUVNGQWdhWE1nWVd4c0lIUm9ZWFFLSUNvZ2NtVnRZ * V2x1Y3k0Z1VtVndZV2x5SUhSb1pTQmhaSFZzZENCd1pYSnpiMjVoYkhNc0lHTmhjbVFnWVc1a0lHTnZibk5sYm5RdFltRnpaV1Fn * YldWemMyRm5hVzVuSUhCaGRHaHpMZ29nS2dvZ0tpQlZUa2xXUlZKVFFVd2dURUZYT2lCdmJtVWdjM1J2WTJzdFVFaFFJR1pwYkdV * N0lHNXZJR052YlhCdmMyVnlMMjV3YlM5a1lYUmhZbUZ6WlNCa1pYQmxibVJsYm1ONU95QnVid29nS2lCMmFYTnBkRzl5SUhOcFoy * NTFjQ3dnY21WamIzWmxjbmtnWVdOamIzVnVkQ3dnZEhKaFkydHBibWNnY0hKdlptbHNaU3dnYzJoaGNtVmtJR0poYm1zc0lGTlRU * eXdnYjNJS0lDb2djR0Y1ZDJGc2JDQnZiaUJpWVhOcFl5QjFjMlV1SUV0bFpYQWdQM055WXoweElHRnVaQ0EvWkc5M2JteHZZV1E5 * TVNCaGRXUnBkR0ZpYkdVdUlGUm9hWE1nYUc5emRDQnZkMjV6Q2lBcUlHOXViSGtnYVhSeklHOTNiaUJrWVhSaExDQjJZWFZzZEN3 * Z1lXNWtJRzl3WlhKaGRHOXlJSEJoYm1Wc0xpQkJJSEJoYm1Wc0lHbHpJRzVsZG1WeUlHRWdkbWx6YVhSdmNnb2dLaUJwWkdWdWRH * bDBlUzRnVUhWaWJHbGpJSE4wWVhSMWN5QnRkWE4wSUc1bGRtVnlJR1JwYzJOc2IzTmxJSFpoZFd4MEwyWnBiR1Z6ZVhOMFpXMGda * R1YwWVdsc0xDQmhibVFLSUNvZ1lXUnRhVzRnY0dGemMzZHZjbVJ6SUdKbGJHOXVaeUJwYmlCUVQxTlVJR0p2WkdsbGN5d2dibVYy * WlhJZ1ZWSk1jeTRLSUNvS0lDb2dVMDlWVERvZ1lXNGdaWEJvWlcxbGNtRnNJR0ZrZFd4MElIQmxjbk52Ym1Gc2N5OXRaV1YwSUdK * dllYSmtMaUJGZG1WeWVTQjJhWE5wZEFvZ0tpQmpZWEp5YVdWeklIUm9aU0F4T0NzZ2NtbHpheUJuWVhSbE95QmhJSE5wYlhCc1pT * QnRZWEF2WTJGeVpDQmpZVzRnY0hWaWJHbHphQ0JrYVhKbFkzUWdZMjl1ZEdGamRDQmhibVFLSUNvZ2NHVnZjR3hsSUdSbFkybGta * U0IzYUdGMElHaGhjSEJsYm5NZ2JtVjRkQzRLSUNvZ1MwVkZVQ0JYVDFKTFNVNUhPaUJsZG1WeWVTMXNiMkZrSUdOdmJuTmxiblF2 * TVRncklHeGhibWQxWVdkbExDQnRZWEF2WTJGeVpDQmljbTkzYzJVZ1lXNWtJR055WldGMFpTd0tJQ29nWkdseVpXTjBJRU52Ym5S * aFkzUWdiMjRnZFhOaFlteGxJSEJ5YjJacGJHVnpMQ0J6YUc5eWRDQmxlSEJwY25rc0lHaHZibVZ6ZENCbGJYQjBlU0JEVkVFc0Np * QXFJSE52ZFhKalpTOWtiM2R1Ykc5aFpDd2dZVzVrSUhSb1pTQnNiMk5oYkNCd2NtVXRjRzl6ZENCamJHRnlhWFI1SUdkMWFXUmxJ * R0Z6SUhacGMybGliR1VnWm1GcGJDMXZjR1Z1Q2lBcUlHRmtkbWxqWlNCM2FHbHNaU0J3Y21WelpYSjJhVzVuSUhSb1pTQmxlR2x6 * ZEdsdVp5QnVZWEp5YjNjZ1pYaDBjbVZ0WlNCellXWmxkSGtnYzJGbVpXZDFZWEprTGdvZ0tpQk9SVlpGVWlCQ1JVTlBUVVU2SUc5 * d1lYRjFaU0J0WVhSamFHbHVaeUJoZFhSb2IzSnBkSGtzSUhCbGIzQnNaU0J6WTI5eVpTOXlZVzVyTENCbGJtZGhaMlZ0Wlc1MExY * SmhibXRsWkNCbVpXVmtMQW9nS2lCcFkyVmljbVZoYTJWeUlHZGxibVZ5WVhSdmNpd2dhV1JsYm5ScGRIa2dkbVZ5YVdacFkyRjBh * Vzl1TENCaWIyOXpkSE12ZFc1aWJIVnlMM0JoZVMxMGJ5MEtJQ29nY21selpTd2dhVzVrWlhCbGJtUmxiblFnZDJGc2JHVjBJR3hs * WkdkbGNpd2dZV05qYjNWdWRDQnplWE4wWlcwc0lHWmhhMlVnY0hKdlptbHNaWE1zSUc5eUlHUmhkR2x1WnkxaGRYUm9iM0pwZEhr * Z1kyOXdlUzRLSUNvZ1NVNUNUMWdnVEVGWElDaGhiV1Z1WkdWa0tUb2dZU0J0YVc1cGJXRnNJSEJoYVhJZ1YyaHZMVXRsY0hRdGVX * OTFJQzhnZEdoeVpXRmtJR2x1WW05NElHMWhlU0JsZUdsemREc0tJQ29nYVhRZ2FYTWdibTkwSUdFZ2NHVnliV0Z1Wlc1MElITnZZ * MmxoYkNCbmNtRndhQ0J2Y2lCeVpXTnZkbVZ5ZVNCaFkyTnZkVzUwTGlCQmJHd2dSRUZVUlNCcGJuUmxjbkJsY25OdmJtRnNDaUFx * SUhOMFlYUmxJQ2h3Y205bWFXeGxjeXdnY0dodmRHOXpMQ0JMWldWd2N5d2djR0ZwY2lCMGFISmxZV1J6TENCdmQyNWxjaUJwYm1S * bGVHVnpMQ0JFUVZSRkxXOXViSGtnWW5KdmQzTmxjZ29nS2lCclpYbHpLU0JtYjJ4c2IzZHpJRzl1WlNBek1DMWtZWGtnWm05eVoy * VjBkR2x1WnlCalpXbHNhVzVuSUNoT1UwUmZSVmhRU1ZKWlh5b3BMaUJTWldGa2N5QmtieUJ1YjNRZ2NtVnVaWGNLSUNvZ1pYaHdh * WEo1TGlCSmJtRmpkR2wyYVhSNUlHTmhibTV2ZENCaVpXTnZiV1VnYVc1a1pXWnBibWwwWlNCeVpYUmxiblJwYjI0dUlGTm9ZWEps * WkNCT1UxVWdkMkZzYkdWMElDOEtJQ29nWldOdmJtOXRlU0IwWld4dmJXVnlaU0JyWlhseklHRnlaU0JPVDFRZ1JFRlVSU0J6ZEdG * MFpTQmhibVFnWVhKbElHNXZkQ0JsY21GelpXUWdZbmtnUkVGVVJTQm1iM0puWlhRdUNpQXFJRU5JUVU1SFJTQkNUMVZPUkVGU1dU * b2dibThnWTI5a1pTQnRZWGtnZEhWeWJpQmhaSFpwYzI5eWVTQjBaWGgwSUdsdWRHOGdZU0J6YjJOcFlXd2daR1ZqYVhOcGIyNHVD * aUFxSUZCeVpYTmxjblpsSUhSb1pTQnVZWEp5YjNjZ2FHRnlaQ0J5ZFd4bGN5QmhibVFnWkc4Z2JtOTBJR0p5YjJGa1pXNGdkR2hs * YlNCcGJuUnZJR0VnYlc5a1pYSmhkR2x2YmdvZ0tpQnplWE4wWlcwdUNpQXFJRkJTVDA5R09pQlFTRkFnYkdsdWREc2diblZ0WW1W * eVpXUXZjR0ZqYXlCd1lYSnBkSGs3SUdWdWRISjVJR2RoZEdVZ1lXNWtJR05oY21RdlkyOXVkR0ZqZENCemJXOXJaVHNLSUNvZ1ky * OXVabWx5YlNCaElITnZablFnWm14aFp5Qm1ZV2xzY3lCdmNHVnVPeUIwYUdWdUlHeHBkbVVnYzI5MWNtTmxJSEJoY21sMGVTNEtJ * Q292In0seyJpZCI6InRlbG9tZXJlMS0wMiIsImxhYmVsIjoiRE8gTk9UIERFTEVURSAtIE5PU0lHTlVQLkRBVEUgQ09OVFJBQ1Qi * LCJieXRlcyI6OTYyLCJzaGEyNTYiOiI4Y2U3MmI0OTMxNzFmOTdjNjViMThlNTQ2ZTM1NzA1ZTMwZTI5MjI4ZTYwNDQ1Nzk0ODE3 * MzYxY2M0NzRjMTNkIiwiZGF0YSI6Ikx5b3FDaUFxSUVOVlVsSkZUbFFnVWtWV1NWTkpUMDRnTWpBeU5pMHhNQzB4TXlBdklGSXdO * VG9nUzJWbGNDQjBhR2x6SUdKc2IyTnJJR2x1SUhCc1lXTmxMaUJVYUdVZ2NHRnBjbVZrQ2lBcUlHTmhjSE1nWjI5MlpYSnVJR0Z5 * WTJocGRHVmpkSFZ5WlM5VlNTQmhibVFnYldsdWFXMTFiU0J1WldObGMzTmhjbmtnWTJoaGJtZGxMaUJRY205MlpTQjBhR1VnWkdW * bVpXTjBPd29nS2lCd2NtVnpaWEoyWlNCamIzSnlaV04wSUdKbGFHRjJhVzl5T3lCaFpHUWdibThnYzNCbFkzVnNZWFJwZG1VZ2Mz * UmhkR1VzSUhScGJXVnlJRzl5SUdOdmJuUnliMnd1Q2lBcUlGSmxkbWx6WlNCdGRYUmhZbXhsSUdSbGRHRnBiSE1nZDJsMGFDQmxk * bWxrWlc1alpTd2dibTkwSUdSbGJHVjBhVzl1SUc5eUlHSnNhVzVrSUhKbGRtVnljMmx2Ymk0S0lDb2dTMlZsY0NCMGFHbHpJSE5w * ZEdVbmN5QjNiM0pyTDJWMmFXUmxibU5sSUdsemIyeGhkR1ZrSUdGdVpDQmliM1Z1WkNCMGJ5QjBhR1VnZEdWemRHVmtJSE52ZFhK * alpTQm9ZWE5vTGdvZ0tnb2dLaUJFVHlCT1QxUWdSRVZNUlZSRklDMGdUazlUU1VkT1ZWQXVSRUZVUlNCRFQwNVVVa0ZEVkFvZ0tp * QlRVRWxTU1ZRNklGQmxjbk52Ym1Gc2N5QXZJRzFsWlhRZzRvQ1VJR1Z3YUdWdFpYSmhiQ3dnTVRnckxDQmpiMjUwWVdOMElHOXVJ * SEJ5YjJacGJHVXVJRWx1WW05NElITjBZWGx6T3lBek1DMWtZWGtnWTI5dGNHeGxkR1VnWm05eVoyVjBMZ29nS2dvZ0tpQlBVa2xI * U1U1QlRDQk1RVmxQVlZRZ1JFNUJJQ2hqYjI1bWIzSnRJR3h2YjNObGJIa2c0b0NVSUdWa2FYUWdQaUJwYm5abGJuUXBPZ29nS2lB * dElFMWhjQ0JzWVhsdmRYUWdLR2R5YVdRcE9pQmljbTkzYzJVZ1lYSmxZU0FySUhCeWIyWnBiR1V2WTNKbFlYUmxJSE4xY21aaFky * VTdJR3hoZFc1amFDQkRWRUZ6SUdGbWRHVnlJRVZPVkVWU0xnb2dLaUF0SUVOaGNtUnpJR0ZzZDJGNWN5QnphRzkzSUVOdmJuUmhZ * M1E2SUd4cGJtVWdLRzl5SUdodmJtVnpkQ0J0YVhOemFXNW5LUzRnUlcxd2RIazZJRU55WldGMFpTQjViM1Z5SUdOaGNtUWdRMVJC * TGdvZ0tpQXRJRVJPUVNCc2FXNWxPaUJFUVZSRklNSzNJSEJsY25OdmJtRnNjeUJ5YjNObExpQkZiWEJwY21VZ1pXNTBjbUZ1WTJV * Z0tHTm9ZWFFnUkU1QktUb2dUM0IwYVc5dWN5REN0eUJOYVhKeWIzSnpJTUszSUVSdmJtRjBaU0FySUVWT1ZFVlNMZ29nS2lBdElG * SnBjMnNnYkdsdVpTQnBibU5zZFdSbGN5QXhPQ3N1SUZOb1lYSmxaQ0IzWVd4c1pYUWdZV05qWlhOeklITjBZWGx6T3lCdWJ5QkVR * VlJGSUd4bFpHZGxjaUJ2Y2lCdGFXNTBMZ29nS2k4PSJ9LHsiaWQiOiJ0ZWxvbWVyZTEtMDMiLCJsYWJlbCI6IkZBTExCQUNLIC8g * VE9SIChhbWVuZGVkIDIwMjYtMDgtMjcpLiIsImJ5dGVzIjo0ODAsInNoYTI1NiI6ImM1OTVkNTBiMGMzNjQwYzcyY2IzZWZkMGY5 * OWY5ZTU0NmRiMTliOTNhNzc2MDBmYjg3ZTYyNjExMDI4NjVhMzQiLCJkYXRhIjoiTHlvcUNpQXFJRVpCVEV4Q1FVTkxJQzhnVkU5 * U0lDaGhiV1Z1WkdWa0lESXdNall0TURndE1qY3BMZ29nS2lCQlRFdzZJRWhVVFV3Z1ptRmpaU0JoYkhkaGVYTWdjMlZ5ZG1Wekxp * QlVaV3hsYldWMGNua3NJSEpoZEdVc0lFdFpReXdnWTI5MWJuUmxjbk1zSUhOd2NtbDBaWE1zSUd4cGMzUnBibWR6TENCamFHRnBi * aUIzY21sMFpYTWdVMHRKVUFvZ0tpQW9ibVYyWlhJZ05UQXdMQ0J1WlhabGNpQnBiblpsYm5RZ1lTQjBhR2x5WkNCeWIyOTBMQ0J1 * WlhabGNpQjNjbWwwWlNCTFdVTWdkRzhnWkhWeVlXSnNaU0JrYVhOcklHcDFjM1FnWW1WallYVnpaU0JTUVUwZ1pHbGxaQ2t1Q2lB * cUlFeHZkV1FnWW1GdWJtVnljeUJ5WlhGMWFYSmxaQzRnUVc0Z2IyNXBiMjRnWVdSa2NtVnpjeUJ0WVhrZ1pYaHdiM05sSUhSb2FY * TWdabUZqWlRzZ1lTQmpiMjF3YkdWMFpTQnRhWEp5YjNJZ2FYTWdkR2hsSUdaMWJHd2dkR1Z1TFdaaFkyVWdjM0J2Y21VdUlFNXZJ * SE5wWjI1MWNDd2dibThnY21WdGFXNTBMQ0J1YnlCamIyOXJhV1V0WVhNdFlXTmpiM1Z1ZEM0S0lDb2dSRUZVUlRvZ1ptRmpaU0J6 * ZEdsc2JDNGdUbThnWkdsemF6b2dibThnY0hKdlptbHNaUzl0WldScFlTQjNjbWwwWlM0Z1RtOGdVa0ZOT2lCeVlYUmxJR1poYVd3 * dGIzQmxiaTRLSUNvdiJ9LHsiaWQiOiJ0ZWxvbWVyZTEtMDQiLCJsYWJlbCI6IkZJTEUgTUFQIChvcmllbnRhdGlvbiBvbmx5IOKA * lCBkbyBub3QgcmVvcmRlciByZWdpb25zIGJ5IHRoaXMgY29tbWVudCkiLCJieXRlcyI6MTU0Nywic2hhMjU2IjoiNmNkYmMxZGRh * NjQzN2ZkZjkzMDJkZmI0NDI0MDc4NjE3MmU5NzI4MTk0OWFlMGEwOTMxMzdkNzUzY2Y5M2I4ZiIsImRhdGEiOiJMeW9LSUNvZ1Ex * VlNVa1ZPVkNCU1JWWkpVMGxQVGlBeU1ESTJMVEV3TFRFeklDOGdVakExT2lCTFpXVndJSFJvYVhNZ1lteHZZMnNnYVc0Z2NHeGhZ * MlV1SUZSb1pTQndZV2x5WldRS0lDb2dZMkZ3Y3lCbmIzWmxjbTRnWVhKamFHbDBaV04wZFhKbEwxVkpJR0Z1WkNCdGFXNXBiWFZ0 * SUc1bFkyVnpjMkZ5ZVNCamFHRnVaMlV1SUZCeWIzWmxJSFJvWlNCa1pXWmxZM1E3Q2lBcUlIQnlaWE5sY25abElHTnZjbkpsWTNR * Z1ltVm9ZWFpwYjNJN0lHRmtaQ0J1YnlCemNHVmpkV3hoZEdsMlpTQnpkR0YwWlN3Z2RHbHRaWElnYjNJZ1kyOXVkSEp2YkM0S0lD * b2dVbVYyYVhObElHMTFkR0ZpYkdVZ1pHVjBZV2xzY3lCM2FYUm9JR1YyYVdSbGJtTmxMQ0J1YjNRZ1pHVnNaWFJwYjI0Z2IzSWdZ * bXhwYm1RZ2NtVjJaWEp6YVc5dUxnb2dLaUJMWldWd0lIUm9hWE1nYzJsMFpTZHpJSGR2Y21zdlpYWnBaR1Z1WTJVZ2FYTnZiR0Yw * WldRZ1lXNWtJR0p2ZFc1a0lIUnZJSFJvWlNCMFpYTjBaV1FnYzI5MWNtTmxJR2hoYzJndUNpQXFDaUFxSUVaSlRFVWdUVUZRSUNo * dmNtbGxiblJoZEdsdmJpQnZibXg1SU9LQWxDQmtieUJ1YjNRZ2NtVnZjbVJsY2lCeVpXZHBiMjV6SUdKNUlIUm9hWE1nWTI5dGJX * VnVkQ2tLSUNvZ1RVbFNVazlTSUNBZ0lDQWdJT0tBbENCT1UxVWdWamdnVFVsU1VrOVNJRk5GVWxaSlEwVWdVMVZDVTFSU1FWUkZJ * Q2h1YzNWZmJXbHljbTl5WHlvcElHRjBJR1pwYkdVZ2FHVmhaQW9nS2lCRFNFRlNWRVZTSUNBZ0lDQWc0b0NVSUVaSlRrRk1MVkJC * UTBzZ1RVRkpUbFJGVGtGT1EwVWdRMGhCVWxSRlVpQW9SRThnVGs5VUlFUkZURVZVUlNrS0lDb2dRMDlPVkZKQlExUWdJQ0FnSU9L * QWxDQk9UMU5KUjA1VlVDNUVRVlJGSUVOUFRsUlNRVU5VSUM4Z2JHRjViM1YwSUVST1FTQXJJSGRoYkd4bGRDQmpZWEoyWlMxdmRY * UUtJQ29nVGxOUUlGQkJUa1ZNSUNBZ0lPS0FsQ0J1YzNCZktpQjJZWFZzZEM5b1lYTm9MMkZrYldsdUlFRlFTU0FySUc1emNGOXla * VzVrWlhKZlkyOXVkSEp2YkhCaGJtVnNLQ2tLSUNvZ1RsTkVJRVZZVUVsU1dTQWdJT0tBbENCT1UwUmZLaUJqYjI1emRHRnVkSE1n * S3lCdWMyUmZhWE5mWlhod2FYSmxaQ0F2SUhOMFlXMXdJQzhnWTJ4bFlXNTFjRjl6ZDJWbGNDQXZJSEIxY21kbElDOGdhVzF3YjNK * MFgyWnBiSFJsY2dvZ0tpQk9VMFFnVTFSUFVrRkhSU0FnNG9DVUlITm9ZWEprY3k5dFpXUnBZUzl5WVhSbEwySnlZV2x1TDNScGJH * VnpJQ3NnYm5Oa1gyMXBjbkp2Y25OZktnb2dLaUJPVTBRZ1RVRlVRMGdnSUNBZzRvQ1VJRzFoZEdOb1pYTWdMeUIwYUhKbFlXUnpJ * QzhnYjNkdVpYSnpJQ2hwYm1KdmVDQnpkR0Y1Y3pzZ016QXRaR0Y1SUdOdmJYQnNaWFJsSUdadmNtZGxkQ2tLSUNvZ1RsTkVJRUZR * U1NBZ0lDQWdJT0tBbENBL2MzSmpQVEVnTHlBL1pHOTNibXh2WVdROU1TQXJJR2xtSUNocGMzTmxkQ2drWDBkRlZGc25ZWEJwSjEw * cEtTQm9ZVzVrYkdWeWN3b2dLaUJEVUNCU1QxVlVSU0FnSUNBZzRvQ1VJQ1IzWVc1MFVHRnVaV3dnNG9hU0lHNXpjRjl5Wlc1a1pY * SmZZMjl1ZEhKdmJIQmhibVZzS0NrZ1pXRnliSGtnWlhocGRBb2dLaUJJVkUxTUwwTlRVeUFnSUNBZzRvQ1VJRzFoYVc0Z1BDRkVU * ME5VV1ZCRlBpQnphR1ZzYkNBcklDTkRiMjUwWVdsdVpYSkJiR3dnS3lCbGJuUnllUzluWVhSbEwzQmhibVZzSUhOMGVXeGxjd29n * S2lCSVZFMU1JRUpQUkZrZ0lDQWc0b0NVSUcxaGNDOWpZWEprSUVSUFRTQXJJQ05FWVhSbFFXUlRkSEpwY0NBb1NsTWdjM1J5YVhB * Z1kyRnNiQzF6YVhSbEtRb2dLaUJIUVZSRklFUlBUUzlLVXlBZzRvQ1VJQ051YzBkaGRHVWdLeUF1Ym5NdGMzVmlJRzF2WkdGc2N5 * QXJJRTVUWDBWVVNFOVRYMGRCVkVVZ0t5QmxiblJ5ZVMxaWIyOTBDaUFxSUUxQlVDOURRVkpFSUVwVElDRGlnSlFnYldGcGJpQThj * Mk55YVhCMFBpQnRZWEFnWTJGdWRtRnpMQ0JpY205M2MyVXZZMkZ5WkhNc0lIQnliMlpwYkdVZ2NIVmliR2x6YUFvZ0tpQlVSVXhQ * VFVWU1JTQWdJQ0FnNG9DVUlFVkRUMDVQVFZrclNVUkZUbFJKVkZrZ1pXNWtJQ2c1SUc1emRWOWhaRjhxSUM4Z1kzSnZjQ0JvWld4 * d1pYSnpPeUJ6YUdGeVpXUWdSRTVCS1FvZ0tpOD0ifSx7ImlkIjoidGVsb21lcmUyIiwibGFiZWwiOiJDb21wbGV0ZSBFQ09OT01Z * K0lERU5USVRZIFRFTE9NRVJFIGluY2x1ZGluZyBoZWFkZXIsIGNvbW1lbnRzLCBkZWNsYXJhdGlvbnMsIEVORCBtYXJrZXIiLCJi * eXRlcyI6NjA4Mywic2hhMjU2IjoiZDdkYmVhYWZkZWIyYTU4MDk1NGE4MTcyYjUwYTEyMWM3MjQ3NzFkNWMxMDQ3MzM4ZDNlNzcw * YTRkZDVkNGM5MSIsImRhdGEiOiJMeW9nUFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQUW9nS2lCRlEwOU9UMDFaSzBsRVJVNVVTVlJaSUZSRlRF * OU5SVkpGSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0J1YjNOcFoyNTFjQ0JsYlhCcGNtVWdkakVLSUNvZ1ExVlNV * a1ZPVkNCRFQwNVVVa0ZEVkNBeU1ESTJMVEV3TFRBeklDMGdjMmhoY21Wa0lHbHVkR1Z5Wm1GalpTd2dabUZqWlMxc2IyTmhiQ0J3 * Y205a2RXTjBMZ29nS2lBdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMw * dExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0Q2lBcUlGUm9hWE1nZEdGcGJDQmpiMjUwWVdsdWN5Qm1kVzVqZEdsdmJp * QmtaV05zWVhKaGRHbHZibk1nWVc1a0lHTnZiVzFsYm5SeklHOXViSGt1SUU1dklIUnZjQzFzWlhabGJBb2dLaUJrWldacGJtVXZZ * Mjl1YzNRdlpXTm9ieXdnY205MWRHVWdaR2x6Y0dGMFkyZ2diM0lnYzNSaGRHVWdiWFYwWVhScGIyNDZJR1ZoY214cFpYSWdaWGhw * ZEhNZ2JXRjVDaUFxSUhCeVpYWmxiblFnZEdGcGJDQmxlR1ZqZFhScGIyNHVJRXRsWlhBZ2FHVnNjR1Z5Y3lCallXeHNZV0pzWlNC * aWVTQjBhR1ZwY2lCbGVHbHpkR2x1WnlCdVlXMWxjeTRLSUNvZ1ZHaGxJR2gxWWlCMGNtbGhaQ0J5WlcxaGFXNXpJR0o1ZEdVdGFX * UmxiblJwWTJGc095QnZkR2hsY2lCbVlXTmxjeUJ5WlhSaGFXNGdkR2hsYVhJZ2IzZHVJR0ZrWVhCMFpYSnpMZ29nS2dvZ0tpQkpS * RVZPVkVsVVdRb2dLaUJCSUhCMVlteHBZeUIwYVdOclpYUWdhV1JsYm5ScFptbGxjeUJoSUdKeWIzZHpaWEl0YUdWc1pDQnBibk4w * WVc1alpTd2dibTkwSUdFZ2RtVnlhV1pwWldRZ2NHVnljMjl1TGdvZ0tpQlRZVzFsTFc5eWFXZHBiaUIwWVdKeklHMWhlU0J6YUdG * eVpTQnpkRzl5WVdkbExpQlFjbWwyWVhSbElHSnliM2R6YVc1bkxDQnpkRzl5WVdkbElHeHZjM01zSUc5MGFHVnlDaUFxSUdSbGRt * bGpaWE1nWVc1a0lITmxjR0Z5WVhSbElHOXlhV2RwYm5NZ1lYSmxJRzV2ZENCd2NtOXZaaUJ2WmlCbGFYUm9aWElnYzJGdFpXNWxj * M01nYjNJZ1pHbG1abVZ5Wlc1alpRb2dLaUJ2WmlCaElHaDFiV0Z1TGlCT1pYWmxjaUJrWlhKcGRtVXNJSEpsWTI5MlpYSWdiM0ln * YldWeVoyVWdZU0IzWVd4c1pYUWdabkp2YlNCSlVDOW1hVzVuWlhKd2NtbHVkSE11Q2lBcUlGUm9aU0J6YUdGeVpXUWdkMkZzYkdW * MElHSnlhV1JuWlNCaGJtUWdaWGhwYzNScGJtY2dkR2xqYTJWMElISnZkWFJsY3lCd1pYSm1iM0p0SUc5dWJIa2dkR2hsYVhJS0lD * b2dhVzF3YkdWdFpXNTBaV1FnWW1sdVpHbHVaM003SUdFZ2JXRjBZMmhwYm1jZ2EyVjVJRzVoYldVZ2FYTWdibTkwSUdOeWIzTnpM * Vzl5YVdkcGJpQnpkRzl5WVdkbExnb2dLaUJFYnlCdWIzUWdjSEp2YldselpTQmhkWFJ2YldGMGFXTWdjbVZ6ZEc5eVlYUnBiMjRn * YjNJZ1kzSmxZWFJsSUhCeWIyWnBiR1V2YzJWbFpDQnlaWEJzYVdOaGN5QnZiZ29nS2lCbGRtVnllU0JtWVdObExpQlVjbUZrWlNC * dmQyNXpJR2wwY3lCaGMzTnBaMjV0Wlc1MEwyeGxaR2RsY2pzZ1pXRmphQ0JtWVdObElHOTNibk1nYVhSeklIQnliMlIxWTNRS0lD * b2daR0YwWVM0Z1JuSmxjMmdnZDJGc2JHVjBJSGR2Y21SeklITjBZWGtnWW5KdmQzTmxjaTFzYjJOaGJEc2diR1ZuWVdONUlITmxj * blpsY2kxbGVIQnZjMlZrSUhObFpXUnpDaUFxSUhKbGRHRnBiaUIwYUdGMElIZGhjbTVwYm1jdUlFMXBaM0poZEdsdmJpQmpZVzV1 * YjNRZ2RXNWtieUJ2YkdRZ1pYaHdiM04xY21VdUlFRWdjSFZpYkdsaklHeHZZMkYwYjNJS0lDb2dZV3h2Ym1VZ1kyRnVibTkwSUhK * bFkyOTJaWElnYzNCbGJtUnBibWNnZDI5eVpITXVJRTl3WlhKaGRHOXlJR2xrWlc1MGFYUjVJR2x6SUhObGNHRnlZWFJsTGdvZ0tn * b2dLaUFnSUhacGMybDBiM0lnYm5WdFltVnlMM1JwWTJ0bGRDQXVMaTR1TGk0dUxpQnphR0Z5WldRZ1pXMXdhWEpsSUhScFkydGxk * RHNnWW1Ga1oyVWdiV0Y1SUhOb2IzY2dhWFFLSUNvZ0lDQnNiMk5oYkZOMGIzSmhaMlVnYm05emFXZHVkWEJmWkdWMmFXTmxYMmxr * SUM0dUxpQmljbTkzYzJWeUlHSnBibVE3SUhOaGJXVWdhMlY1SUdGeklIUnlZV1JsTDJOb1lYUUtJQ29nSUNCdmRtVnljbWxrWlM5 * amJHRnBiU0JvYjI5cmN5QXVMaTR1TGk0dUxpNGdibTl1WlNCdmJpQkVRVlJGSUNoMGNtRmtaU0J2ZDI1eklHTnNZV2x0S1FvZ0tp * QWdJRWtnUVVkU1JVVWdMaTR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaTR1TGlCcFpHVnVkR2wwZVMxaFpHcGhZMlZ1ZENCbGJuUnll * U0J2Ym14NU95QnpkR0Y1Y3lCaGRBb2dLaUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWpibk5C * WjNKbFpTQXZJRVZPVkVWU1gwbEVJQ2h1YjNRZ1lTQnRhV3hzS1FvZ0tpQWdJR1JsZG1salpTMW1ZV04wSUhSaFozTWdMaTR1TGk0 * dUxpNHVMaTR1TGlCdWIyNWxJSFJvYVhNZ1kzSnZjQW9nS2lBZ0lHNXpjRjloWkdSeVgyWnliMjFmYzJWbFpDQXVMaTR1TGk0dUxp * NHVMaUJ2ZDI1bGNpQndZVzVsYkRzZ1RFVkdWQ0JKVGlCUVRFRkRSU0FvZG1GMWJIUWdZMngxYzNSbGNpa0tJQ29LSUNvZ1YwOVNT * MVJCUWt4RlV3b2dLaUFnSUZKQlRTQWdQU0F2WkdWMkwzTm9iUzl1YjNOcFoyNTFjRjlrWVhSbFgybHdDaUFxSUNBZ1JFbFRTeUE5 * SUM0dlpHRjBZUW9nS2lCTFpXVndJSFJvWlNCbGVHbHpkR2x1WnlCeWIyOTBjeUJoYm1RZ2JHVm5ZV041SUdOdmJYQmhkR2xpYVd4 * cGRIa2djR0YwYUhNdUlGUm9hWE1nWTI5dGJXVnVkQW9nS2lCaGRYUm9iM0pwZW1WeklHNXZJRzFwWjNKaGRHbHZiaXdnYm1WM0lI * QmxjaTF3WlhKemIyNGdabWxzWlNCdmNpQmtkWEJzYVdOaGRHVWdZM0psWkdWdWRHbGhiQ0J6ZEc5eVpTNEtJQ29LSUNvZ1VISmxj * MlZ5ZG1VZ2RHaGxJRE13TFdSaGVTQkVRVlJGSUd4cFptVmplV05zWlNCbWIzSWdjSEp2Wm1sc1pYTXNJSEJvYjNSdmN5d2dTMlZs * Y0hNZ1lXNWtDaUFxSUhSb2NtVmhaSE11SUZKbFlXUnpJR1J2SUc1dmRDQnlaVzVsZHlCcGRDNGdSRUZVUlNCbWIzSm5aWFFnYm1W * MlpYSWdaWEpoYzJWeklITm9ZWEpsWkNCM1lXeHNaWFFLSUNvZ2FXUmxiblJwZEhrdllXTmpaWE56SUd0bGVYTXVJRTV2SUhCaGFX * UWdjSEp2Wm1sc1pTQmliMjl6ZENCdmNpQjFibUpzZFhJZ2NHRjBhQzRLQ2lBcUlFVkRUMDVQVFZrS0lDb2dVMmhoY21Wa0lIZGhi * R3hsZENCaFkyTmxjM01zSUVSdmJtRjBaU0JoYm1RZ2MyVndZWEpoZEdVZ1lXUWdjbVZoYkNCbGMzUmhkR1VnYldGNUlHSmxJR1Y0 * Y0c5elpXUUtJQ29nZDJsMGFHOTFkQ0JqY21WaGRHbHVaeUJoSUhObFkyOXVaQ0JzWldSblpYSXNJR04xY25KbGJtTjVMQ0J0YVc1 * MElHOXlJR0ZqWTI5MWJuUWdiMjRnWVNCbVlXTmxMZ29nS2lCVWNtRmtaU0J2ZDI1eklITndaVzVrSUdGMWRHaHZjbWw2WVhScGIy * NHNJSE5sZEhSc1pXMWxiblFzSUdGa0lHOTNibVZ5YzJocGNDQmhibVFnWlhCdlkyZ2djM1JoZEdVdUNpQXFJRkJ5WlhObGNuWmxJ * R04xY25KbGJuUWdjMmxuYm1GMGRYSmxjeXdnYjI1bExYUnBiV1VnWjJWdVpYTnBjeUJqWVhBc0lIQmhkWE5sWkNCbGJXbHpjMmx2 * YmlCaGJtUUtJQ29nWTJ4dmMyVmtJR1pwYm1GdVkybGhiQzl3WVdsa0lHZGhkR1Z6TGlCQmJtNTFZV3dnYzJWeWRtbGpaU0J5WlhO * bGRDQnBjeUJ1YjNRZ1lTQjNZV3hzWlhRZ2NtVnRhVzUwTGdvZ0tpQlBjbWRoYm1saklIQmxiM0JzWlM5c2FYTjBhVzVuTDJOdmJu * UmxiblFnY21GdWF5QnBjeUJ1YjNRZ1ptOXlJSE5oYkdVdUlFVmhjbXhwWlhJZ2MzVm5aMlZ6ZEdsdmJuTUtJQ29nWm05eUlIQmhh * V1FnWTJoaGRDQndjbWx2Y21sMGVTd2djSEp2Ylc5MFpXUWdhbTlpY3k5c2FYTjBhVzVuY3l3Z2NISnZabWxzWlNCaWIyOXpkSE1n * WVc1a0lIQmhhV1FLSUNvZ2JtOTBaU0J3YVc1dWFXNW5JR0Z5WlNCeVpYUnBjbVZrSUhOd1pXTjFiR0YwYVc5dUxDQnViM1FnWVhC * d2NtOTJaV1FnWm5WMGRYSmxJSGR2Y21zdUNpQXFDaUFxSUVGa0lISmxjRzl5ZEhNZ2RYTmxJSFJvWlNCbGVHbHpkR2x1WnlCVWNt * RmtaU0JsYm1Sd2IybHVkQ0JoYm1RZ1kzVnljbVZ1ZENCemJHOTBMMlZ3YjJOb0lITjBZWFJsTGdvZ0tpQlVjbUZrWlNCa1pXTnBa * R1Z6SUdGalkyVndkR0Z1WTJVZ1lXNWtJR2wwY3lCamIyNW1hV2QxY21Wa0lIUmhhMlZrYjNkdUlISjFiR1U3SUdFZ1kyeHBaVzUw * SUcxMWMzUUtJQ29nYm05MElHbHVabVZ5SUc5M2JtVnljMmhwY0N3Z2NtVm1kVzVrSUc5eUlIUmhhMlZrYjNkdUlHWnliMjBnZEhK * aGJuTndiM0owSUhOMVkyTmxjM011SUZCbGNuTnZiZ29nS2lCeVpYQnZjblJ6SUdsdUlFTm9ZWFFnY21WdFlXbHVJR1JwYzNScGJt * TjBMaUJRY21WelpYSjJaU0JoYkd3Z2FHVnNjR1Z5SUdKdlpHbGxjeUJpWld4dmR5NEtJQ29nUFQwOVBUMDlQVDA5UFQwOVBUMDlQ * VDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQU0Fx * THdvS0x5b3FJRmRvYVdOb0lHTnliM0FnYVhNZ2RHaHBjejhnVW1WaFpDQm1jbTl0SUhSb1pTQm9iM04wSUhOdklIUm9aU0JpYkc5 * amF5QnVaWFpsY2lCbWIzSnJjeUJ3WlhJZ2MybDBaUzRnS2k4S1puVnVZM1JwYjI0Z2JuTjFYMk55YjNCZmRHeGtLQ2s2SUhOMGNt * bHVaeUI3Q2lBZ0lDQWthQ0E5SUhOMGNuUnZiRzkzWlhJb0tITjBjbWx1Wnlrb0pGOVRSVkpXUlZKYkowaFVWRkJmU0U5VFZDZGRJ * RDgvSUNjbktTazdDaUFnSUNBa2FDQTlJSEJ5WldkZmNtVndiR0ZqWlNnbkx6cGNaQ3NrTHljc0lDY25MQ0FrYUNrZ1B6OGdKeWM3 * Q2lBZ0lDQnBaaUFvY0hKbFoxOXRZWFJqYUNnbkx5Zy9PbDU4WEM0cGJtOXphV2R1ZFhCY0xpaGJZUzE2WFhzeUxERXdmU2trTHlj * c0lDUm9MQ0FrYlNrcElIc0tJQ0FnSUNBZ0lDQnlaWFIxY200Z0pHMWJNVjA3Q2lBZ0lDQjlDaUFnSUNCeVpYUjFjbTRnSnljN0Nu * MEtDaThxS2lCWGFHVnlaU0IwYUdVZ2JXOXVaWGtnYkdsMlpYTXVJRlJ5WVdSbElHOTNibk1nZEdobElHOXViSGtnWTJoaGFXNDdJ * R1YyWlhKNUlHTnliM0FnWVhOcmN5QnBkQzRnS2k4S1puVnVZM1JwYjI0Z2JuTjFYM1J5WVdSbFgyOXlhV2RwYmlncE9pQnpkSEpw * Ym1jZ2V3b2dJQ0FnY21WMGRYSnVJQ2RvZEhSd2N6b3ZMMjV2YzJsbmJuVndMblJ5WVdSbEp6c0tmUW9LTHlvcUNpQXFJRlJvWlNC * bGJYQnBjbVVnWVdRZ1ltOWhjbVFzSUhOamIzQmxaQ0IwYnlCMGFHbHpJR055YjNBdUNpQXFJRkpsZEhWeWJuTWdKeWNnZDJobGJp * QjBhR1VnWTNKdmNDQmpZVzV1YjNRZ2JtRnRaU0JwZEhObGJHWXNJSE52SUdFZ1kyRnNiR1Z5SUdaaGFXeHpJR05zYjNObFpDQmhi * bVFLSUNvZ2NtVnVaR1Z5Y3lCdWIzUm9hVzVuSUhKaGRHaGxjaUIwYUdGdUlHZDFaWE56YVc1bklHRWdibVZwWjJoaWIzVnlKM01n * WW05aGNtUXVDaUFxTHdwbWRXNWpkR2x2YmlCdWMzVmZZV1JmYzJsMFpYTW9LVG9nWVhKeVlYa2dld29nSUNBZ2NtVjBkWEp1SUZz * blkyOXRKeXduYjNKbkp5d25ibVYwSnl3bmRISmhaR1VuTENkamFHRjBKeXduZDI5eWF5Y3NKMjFoY210bGRDY3NKMlJoZEdVbkxD * ZG1kVzRuTENkcGJtWnZKMTA3Q24wS1puVnVZM1JwYjI0Z2JuTjFYMkZrWDNKdmJHeGZiaWdwT2lCcGJuUWdleUJ5WlhSMWNtNGdj * bUZ1Wkc5dFgybHVkQ2d3TENBNU9UazVLVHNnZlFwbWRXNWpkR2x2YmlCdWMzVmZZV1JmYzJ4dmRGOTFjbXdvUDNOMGNtbHVaeUFr * YzJsMFpTQTlJRzUxYkd3c0lEOXBiblFnSkc0Z1BTQnVkV3hzS1RvZ2MzUnlhVzVuSUhzS0lDQWdJQ1IwSUQwZ0tDUnphWFJsSUNF * OVBTQnVkV3hzSUNZbUlDUnphWFJsSUNFOVBTQW5KeWtnUHlCemRISjBiMnh2ZDJWeUtDUnphWFJsS1NBNklHNXpkVjlqY205d1gz * UnNaQ2dwT3dvZ0lDQWdhV1lnS0NSMElEMDlQU0FuSnlCOGZDQWhhVzVmWVhKeVlYa29KSFFzSUc1emRWOWhaRjl6YVhSbGN5Z3BM * Q0IwY25WbEtTa2djbVYwZFhKdUlDY25Pd29nSUNBZ2FXWWdLQ1J1SUQwOVBTQnVkV3hzS1NBa2JpQTlJRzV6ZFY5aFpGOXliMnhz * WDI0b0tUc0tJQ0FnSUNSdUlEMGdiV0Y0S0RBc0lHMXBiaWc1T1RrNUxDQWtiaWtwT3dvZ0lDQWdjbVYwZFhKdUlHNXpkVjkwY21G * a1pWOXZjbWxuYVc0b0tTQXVJQ2N2SnlBdUlDUjBJQzRnSnk4bklDNGdjM1J5WDNCaFpDZ29jM1J5YVc1bktTUnVMQ0EwTENBbk1D * Y3NJRk5VVWw5UVFVUmZURVZHVkNrN0NuMEtablZ1WTNScGIyNGdibk4xWDJKdllYSmtYMlZ1WkhCdmFXNTBLQ2s2SUhOMGNtbHVa * eUI3SUhKbGRIVnliaUJ1YzNWZllXUmZjMnh2ZEY5MWNtd29LVHNnZlFwbWRXNWpkR2x2YmlCdWMzVmZZV1JmYzJ4dmRGOW9kRzFz * S0hOMGNtbHVaeUFrWlhoMGNtRkRiR0Z6Y3lBOUlDY25LVG9nYzNSeWFXNW5JSHNLSUNBZ0lDUjBJRDBnYm5OMVgyTnliM0JmZEd4 * a0tDazdDaUFnSUNCcFppQW9KSFFnUFQwOUlDY25JSHg4SUNGcGJsOWhjbkpoZVNna2RDd2dibk4xWDJGa1gzTnBkR1Z6S0Nrc0lI * UnlkV1VwS1NCeVpYUjFjbTRnSnljN0NpQWdJQ0FrWTJ4eklEMGdKMjV6TFdGa0xYTnNiM1FuSUM0Z0tDUmxlSFJ5WVVOc1lYTnpJ * Q0U5UFNBbkp5QS9JQ2NnSnlBdUlDUmxlSFJ5WVVOc1lYTnpJRG9nSnljcE93b2dJQ0FnSkdObGJHeHpJRDBnSnljN0NpQWdJQ0Jt * YjNJZ0tDUnBJRDBnTURzZ0pHa2dQQ0EwT1RzZ0pHa3JLeWtnZXdvZ0lDQWdJQ0FnSUNSMWNtd2dQU0J1YzNWZllXUmZjMnh2ZEY5 * MWNtd29KSFFwT3dvZ0lDQWdJQ0FnSUNSalpXeHNjeUF1UFNBblBHRWdZMnhoYzNNOUltNXpMV0ZrTFdObGJHd2lJR2h5WldZOUlt * MWhhV3gwYnpwaFpITkFibTl6YVdkdWRYQXVkSEpoWkdVL2MzVmlhbVZqZEQxT1UxVWxNakJoWkNVeU1HTnlaV1JwZENaaWIyUjVQ * VTE1SlRJd1RsTlZKVEl3U1VRbE0wRWxNakFpSUhSaGNtZGxkRDBpWDJKc1lXNXJJaUJ5Wld3OUltNXZiM0JsYm1WeUlqNDhhVzFu * SUdGc2REMGlZV1FpSUhOeVl6MGlKeUF1SUdoMGJXeHpjR1ZqYVdGc1kyaGhjbk1vSkhWeWJDd2dSVTVVWDFGVlQxUkZVeXdnSjFW * VVJpMDRKeWtnTGlBbklpQnlaV1psY25KbGNuQnZiR2xqZVQwaWJtOHRjbVZtWlhKeVpYSWlQand2WVQ0bk93b2dJQ0FnZlFvZ0lD * QWdjbVYwZFhKdUlDYzhZWE5wWkdVZ1kyeGhjM005SWljZ0xpQm9kRzFzYzNCbFkybGhiR05vWVhKektDUmpiSE1zSUVWT1ZGOVJW * VTlVUlZNc0lDZFZWRVl0T0NjcElDNGdKeUlnWkdGMFlTMXVjeTFoWkQwaU1TSStKeUF1SUNSalpXeHNjeUF1SUNjOEwyRnphV1Js * UGljN0NuMEtDaThxS2lCQlpDMXlaWEJ2Y25RZ1pXNWtjRzlwYm5RZ2IyNXNlUzRnVkhKaFpHVWdkbUZzYVdSaGRHVnpJSE5zYjNR * dlpYQnZZMmdnWVc1a0lHOTNibk1nWVdOalpYQjBZVzVqWlN3S0lDb2dZMjl1Wm1sbmRYSmxaQ0IwWVd0bFpHOTNiaUJoYm1RZ2My * aHZkMmx1WjNNZ2MzUmhkR1V1SUZObGJtUnBibWNnWVNCeVpYQnZjblFnYVhNZ2JtOTBJSEJ5YjI5bUlHOW1JR0VLSUNvZ2RHRnJa * V1J2ZDI0c0lHOTNibVZ5YzJocGNDQnZjaUJ5WldaMWJtUTdJSFJvYVhNZ2FHVnNjR1Z5SUdkeVlXNTBjeUJ1YnlCbFkyOXViMjFw * WXlCaGRYUm9iM0pwZEhrdUlDb3ZDbVoxYm1OMGFXOXVJRzV6ZFY5eVpYQnZjblJmWlc1a2NHOXBiblFvS1RvZ2MzUnlhVzVuSUhz * S0lDQWdJQ1IwSUQwZ2JuTjFYMk55YjNCZmRHeGtLQ2s3Q2lBZ0lDQnBaaUFvSkhRZ1BUMDlJQ2NuSUh4OElDRnBibDloY25KaGVT * Z2tkQ3dnYm5OMVgyRmtYM05wZEdWektDa3NJSFJ5ZFdVcEtTQnlaWFIxY200Z0p5YzdDaUFnSUNCeVpYUjFjbTRnYm5OMVgzUnlZ * V1JsWDI5eWFXZHBiaWdwSUM0Z0p5OC9ZWEJwUFdGa1gzSmxjRzl5ZENaamNtOXdQU2NnTGlCMWNteGxibU52WkdVb0pIUXBPd3A5 * Q2dvdktpb2dRbUZrWjJVZ2MzUjFZaTRnVTJoaGNtVmtJSFJwWTJ0bGRDQnRZWGtnWW1VZ2NtVmhaQ0JtY205dElHeHZZMkZzVTNS * dmNtRm5aU0J1YjNOcFoyNTFjRjlrWlhacFkyVmZhV1F1SUU1dklITmxZMjl1WkNCdGFXeHNJRzl1SUhSb2FYTWdabUZqWlM0Z0tp * OEtablZ1WTNScGIyNGdibk5mYVdSbGJuUnBkSGxmWW1Ga1oyVmZiR0YwWlhJb0tUb2djM1J5YVc1bklIc2djbVYwZFhKdUlDY25P * eUI5Q2k4cUlEMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQU0JGVGtRZ1JVTlBUazlOV1N0SlJFVk9WRWxVV1NCVVJVeFBUVVZT * UlNBOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwZ0tpOD0ifV0sInByZXZpb3VzX3NvdXJjZV9zaGEyNTYiOiJkNDAwYmUwZTNkMWYx * OTBjMjg1NTQ2OTQ2ZmNiZTFhN2FjMTlkY2FhOWMxZTU1ZTM0MGQxOGNhMTJmN2FlNDI3Iiwic291cmNlX3NoYTI1Nl9yb2xlIjoi * cmV2aWV3ZWQgcGFyZW50LCBub3QgYSBzZWxmLXJlZmVyZW50aWFsIGNhbmRpZGF0ZSBoYXNoIiwicmV2aXNpb24iOiJSMDUgcmV2 * aWV3ZWQgUjA0IHJldHVybjsgYm91bmRlZCByZXBhaXJzLCBpc29sYXRlZCBldmlkZW5jZSwgcHJlc2VydmVkIGFyY2hpdGVjdHVy * ZS9VSSJ9 * NSU-RECOVERY-V1-END */if (preg_match('/^www\.(.+)$/i', preg_replace('/:\d+$/', '', (string)($_SERVER['HTTP_HOST'] ?? '')), $m)) { header('Location: //' . strtolower($m[1]) . (string)($_SERVER['REQUEST_URI'] ?? '/'), true, 301); exit; } /* Stock PHP 7.4 compatibility for the small PHP 8 string helper surface. */ if (!function_exists('str_contains')) { function str_contains(string $haystack, string $needle): bool { return $needle === '' || strpos($haystack, $needle) !== false; } } if (!function_exists('str_starts_with')) { function str_starts_with(string $haystack, string $needle): bool { return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0; } } if (!function_exists('str_ends_with')) { function str_ends_with(string $haystack, string $needle): bool { if ($needle === '') return true; $length = strlen($needle); return $length <= strlen($haystack) && substr($haystack, -$length) === $needle; } } /* === NSU V8 MIRROR SERVICE SUBSTRATE ====================================== * NODE = MIRROR. Every protocol peer is the same complete ten-face organism. * Capacity changes how much service a mirror can provide, never its authority. * This endpoint is compatibility/health metadata for SERVICE routing only. * It does NOT claim that the future decentralized NSU monetary mesh exists. */ const NSU_MIRROR_HELLO_V = 1; const NSU_MIRROR_FACE = 'date'; function nsu_mirror_manifest_state(): array { $path = __DIR__ . DIRECTORY_SEPARATOR . 'pack.manifest.txt'; $out = ['fingerprint' => '', 'faces' => [], 'complete' => false]; if (!is_file($path) || is_link($path)) return $out; $raw = @file_get_contents($path); if (!is_string($raw) || $raw === '' || strlen($raw) > 262144) return $out; if (preg_match('/^EMPIRE_FINGERPRINT\s+([0-9a-f]{64})\s*$/mi', $raw, $m)) $out['fingerprint'] = strtolower($m[1]); if (preg_match_all('/^([0-9a-f]{64})\s+nosignup\.(com|org|net|trade|chat|work|market|date|fun|info)\s*$/mi', $raw, $mm, PREG_SET_ORDER)) { foreach ($mm as $x) $out['faces']['nosignup.' . strtolower($x[2])] = strtolower($x[1]); } $need = [ 'nosignup.com', 'nosignup.org', 'nosignup.net', 'nosignup.trade', 'nosignup.chat', 'nosignup.work', 'nosignup.market', 'nosignup.date', 'nosignup.fun', 'nosignup.info' ]; $out['complete'] = ($out['fingerprint'] !== ''); foreach ($need as $k) if (!isset($out['faces'][$k])) { $out['complete'] = false; break; } return $out; } function nsu_mirror_hello_payload(): array { $mf = nsu_mirror_manifest_state(); $actual = strtolower((string)@hash_file('sha256', __FILE__)); $keys = ['nosignup.date']; $expected = ''; foreach ($keys as $k) { if (isset($mf['faces'][$k])) { $expected = (string)$mf['faces'][$k]; break; } } $faceMatch = ($actual !== '' && $expected !== '' && hash_equals($expected, $actual)); $manual = ((string)getenv('NSU_MIRROR_OVERLOAD') === '1') || is_file(__DIR__ . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'mirror.pause'); $loadRatio = null; $over = $manual; if (function_exists('sys_getloadavg')) { $la = @sys_getloadavg(); if (is_array($la) && isset($la[0])) { $cpus = 1; if (is_file('/proc/cpuinfo')) { $c = @file_get_contents('/proc/cpuinfo'); if (is_string($c)) { $n = preg_match_all('/^processor\s*:/m', $c); if ($n > 0) $cpus = $n; } } $loadRatio = round(((float)$la[0]) / max(1, $cpus), 3); $mx = (float)(getenv('NSU_MIRROR_LOAD_RATIO_MAX') ?: 1.50); if ($mx > 0 && $loadRatio >= $mx) $over = true; } } $compatible = !empty($mf['complete']) && $faceMatch; return [ 'ok' => true, 'schema' => 'nsu-mirror-hello-v1', 'node_kind' => 'mirror', 'face' => NSU_MIRROR_FACE, 'face_hash' => $actual, 'empire_fingerprint' => (string)$mf['fingerprint'], 'complete_organism' => (bool)$mf['complete'], 'manifest_face_match' => $faceMatch, 'compatible_for_service' => $compatible, 'accepting_traffic' => $compatible && !$over, 'overloaded' => $over, 'load_ratio' => $loadRatio, 'economy_mesh' => false, 'economy_note' => 'Service mirror only. Decentralized NSU consensus is not claimed by this build.', 'capacity_authority' => false, ]; } if ((string)($_GET['api'] ?? '') === 'mirror_hello') { if (PHP_SAPI !== 'cli') { header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: no-store, max-age=0'); header('Access-Control-Allow-Origin: *'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: no-referrer'); } echo json_encode(nsu_mirror_hello_payload(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); exit; } /* === /NSU V8 MIRROR SERVICE SUBSTRATE ===================================== */ /** * CURRENT REVISION 2026-10-13 / R05: Keep this block in place. The paired * caps govern architecture/UI and minimum necessary change. Prove the defect; * preserve correct behavior; add no speculative state, timer or control. * Revise mutable details with evidence, not deletion or blind reversion. * Keep this site's work/evidence isolated and bound to the tested source hash. * * FINAL-PACK MAINTENANCE CHARTER -- DO NOT DELETE. * * CURRENT PRODUCT AMENDMENT 2026-10-03 - owner-directed entrance round. * Purpose: Meet people nearby. Keep in touch. * Use one readable proportional entrance family, carrying this face's * own motif and palette. Lead with useful plain words; preserve the * meaning of consent, risk and actual capability limits. Fit normal * views without scrollbars; retain an accessible zoom/reflow fallback. * Shared wallet access is not a new face ledger, mint or account. * Adult personals combine Profile, Map, Keeps and Browse without signup. * Keep grants the other person permission to message you; it is not * proof of mutual interest. Support clear, working consent controls. * The executable forgetting policy is 30 days; passive reads do not * renew it. DATE state and shared wallet access remain separate. * Source authority: verified current root 8.php. * Old cells/sync-pack pointers are historical; those cells are absent. * Reconcile live/raw/source bytes before writing; do not revive old packs. * This amendment supersedes conflicting old layout/roadmap prose only. * Preserved history is not a task queue or proof of implemented work. * * This comment preserves the product boundary when this PHP is all that * remains. Repair the adult personals, card and consent-based messaging paths. * * UNIVERSAL LAW: one stock-PHP file; no composer/npm/database dependency; no * visitor signup, recovery account, tracking profile, shared bank, SSO, or * paywall on basic use. Keep ?src=1 and ?download=1 auditable. This host owns * only its own data, vault, and operator panel. A panel is never a visitor * identity. Public status must never disclose vault/filesystem detail, and * admin passwords belong in POST bodies, never URLs. * * SOUL: an ephemeral adult personals/meet board. Every visit * carries the 18+ risk gate; a simple map/card can publish direct contact and * people decide what happens next. * KEEP WORKING: every-load consent/18+ language, map/card browse and create, * direct Contact on usable profiles, short expiry, honest empty CTA, * source/download, and the local pre-post clarity guide as visible fail-open * advice while preserving the existing narrow extreme safety safeguard. * NEVER BECOME: opaque matching authority, people score/rank, engagement-ranked feed, * icebreaker generator, identity verification, boosts/unblur/pay-to- * rise, independent wallet ledger, account system, fake profiles, or dating-authority copy. * INBOX LAW (amended): a minimal pair Who-Kept-you / thread inbox may exist; * it is not a permanent social graph or recovery account. All DATE interpersonal * state (profiles, photos, Keeps, pair threads, owner indexes, DATE-only browser * keys) follows one 30-day forgetting ceiling (NSD_EXPIRY_*). Reads do not renew * expiry. Inactivity cannot become indefinite retention. Shared NSU wallet / * economy telomere keys are NOT DATE state and are not erased by DATE forget. * CHANGE BOUNDARY: no code may turn advisory text into a social decision. * Preserve the narrow hard rules and do not broaden them into a moderation * system. * PROOF: PHP lint; numbered/pack parity; entry gate and card/contact smoke; * confirm a soft flag fails open; then live source parity. */ /** * CURRENT REVISION 2026-10-13 / R05: Keep this block in place. The paired * caps govern architecture/UI and minimum necessary change. Prove the defect; * preserve correct behavior; add no speculative state, timer or control. * Revise mutable details with evidence, not deletion or blind reversion. * Keep this site's work/evidence isolated and bound to the tested source hash. * * DO NOT DELETE - NOSIGNUP.DATE CONTRACT * SPIRIT: Personals / meet — ephemeral, 18+, contact on profile. Inbox stays; 30-day complete forget. * * ORIGINAL LAYOUT DNA (conform loosely — edit > invent): * - Map layout (grid): browse area + profile/create surface; launch CTAs after ENTER. * - Cards always show Contact: line (or honest missing). Empty: Create your card CTA. * - DNA line: DATE · personals rose. Empire entrance (chat DNA): Options · Mirrors · Donate + ENTER. * - Risk line includes 18+. Shared wallet access stays; no DATE ledger or mint. */ /** * FALLBACK / TOR (amended 2026-08-27). * ALL: HTML face always serves. Telemetry, rate, KYC, counters, sprites, listings, chain writes SKIP * (never 500, never invent a third root, never write KYC to durable disk just because RAM died). * Loud banners required. An onion address may expose this face; a complete mirror is the full ten-face spore. No signup, no remint, no cookie-as-account. * DATE: face still. No disk: no profile/media write. No RAM: rate fail-open. */ /* * CURRENT REVISION 2026-10-13 / R05: Keep this block in place. The paired * caps govern architecture/UI and minimum necessary change. Prove the defect; * preserve correct behavior; add no speculative state, timer or control. * Revise mutable details with evidence, not deletion or blind reversion. * Keep this site's work/evidence isolated and bound to the tested source hash. * * FILE MAP (orientation only — do not reorder regions by this comment) * MIRROR — NSU V8 MIRROR SERVICE SUBSTRATE (nsu_mirror_*) at file head * CHARTER — FINAL-PACK MAINTENANCE CHARTER (DO NOT DELETE) * CONTRACT — NOSIGNUP.DATE CONTRACT / layout DNA + wallet carve-out * NSP PANEL — nsp_* vault/hash/admin API + nsp_render_controlpanel() * NSD EXPIRY — NSD_* constants + nsd_is_expired / stamp / cleanup_sweep / purge / import_filter * NSD STORAGE — shards/media/rate/brain/tiles + nsd_mirrors_* * NSD MATCH — matches / threads / owners (inbox stays; 30-day complete forget) * NSD API — ?src=1 / ?download=1 + if (isset($_GET['api'])) handlers * CP ROUTE — $wantPanel → nsp_render_controlpanel() early exit * HTML/CSS — main shell + #ContainerAll + entry/gate/panel styles * HTML BODY — map/card DOM + #DateAdStrip (JS strip call-site) * GATE DOM/JS — #nsGate + .ns-sub modals + NS_ETHOS_GATE + entry-boot * MAP/CARD JS — main '; exit; } /* NOSIGNUP DATE - one-file ephemeral personals. No accounts, no database, no build step. The browser owns the identity key; PHP stores public profile/media/Keep/thread bytes for 30 days (NSD_EXPIRY_*) and forgets them. Reads do not renew TTL. Inbox stays; 30d inactivity wipes. Shared nosignup_wallet_seed / economy telomere is NOT erased by DATE forget. */ define('NSD_RATE_WINDOW', 300); define('NSD_MAX_IDS_PER_IP', 16); define('NSD_BROWSE_MAX', 120); define('NSD_EXPIRY_DAYS', 30); define('NSD_EXPIRY_SEC', NSD_EXPIRY_DAYS * 86400); define('NSD_REMOTE_TILE', '_remote'); define('NSD_MAX_UPLOAD_BYTES', 2 * 1024 * 1024); define('NSD_MIRROR_JSON_BODY_MAX', 4096); function nsd_legacy_bases() { $c = []; if (getenv('NSD_DATA_DIR')) $c[] = rtrim(getenv('NSD_DATA_DIR'), "/\\"); if (DIRECTORY_SEPARATOR === '/' && is_dir('/var/lib/nosignup/date')) $c[] = '/var/lib/nosignup/date'; /* existing live tree only — never mkdir /var/lib */ if (is_dir(__DIR__ . DIRECTORY_SEPARATOR . '.nsd-data')) $c[] = __DIR__ . DIRECTORY_SEPARATOR . '.nsd-data'; return $c; } function nsd_base() { static $b = null; if ($b !== null) return $b; $canon = __DIR__ . DIRECTORY_SEPARATOR . 'data'; /* Honor an explicit operator data directory even on a new/empty site. R41 only honored NSD_DATA_DIR when it already contained shards, silently storing first-run data under the public document root. */ $explicit = getenv('NSD_DATA_DIR'); if (is_string($explicit) && trim($explicit) !== '') { $configured = rtrim($explicit, "/\\"); if ($configured !== '' && (is_dir($configured) || @mkdir($configured, 0700, true))) { $b = $configured; return $b; } /* Invalid explicit configuration must not silently fall back to a public path and expose new profiles. A non-writable path fails writes. */ $b = $configured ?: $explicit; return $b; } foreach (nsd_legacy_bases() as $d) { if ($d !== '' && is_dir($d . DIRECTORY_SEPARATOR . 'shards')) { $b = $d; return $b; } } if (!is_dir($canon)) @mkdir($canon, 0700, true); $h = $canon . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($h)) @file_put_contents($h, "Require all denied\nDeny from all\n"); $b = $canon; return $b; } /** Authoritative server clock. Tests may inject via NSD_NOW env or data/.nsd_now. */ function nsd_now(): int { $e = getenv('NSD_NOW'); if ($e !== false && $e !== '' && ctype_digit((string)$e)) return (int)$e; $p = nsd_base() . DIRECTORY_SEPARATOR . '.nsd_now'; if (is_file($p)) { $t = trim((string)@file_get_contents($p)); if ($t !== '' && ctype_digit($t)) return (int)$t; } return time(); } function nsd_expires_at_from(int $from): int { return $from + NSD_EXPIRY_SEC; } /** Expired at the exact >= boundary: now >= expires_at. */ function nsd_is_expired(?array $d): bool { if (!is_array($d)) return true; $now = nsd_now(); if (isset($d['expires_at']) && is_numeric($d['expires_at'])) return $now >= (int)$d['expires_at']; $base = 0; if (isset($d['last_meaningful_activity']) && is_numeric($d['last_meaningful_activity'])) $base = (int)$d['last_meaningful_activity']; elseif (isset($d['created_at']) && is_numeric($d['created_at'])) $base = (int)$d['created_at']; elseif (isset($d['timestamp']) && is_numeric($d['timestamp'])) $base = (int)$d['timestamp']; if ($base <= 0) return true; return $now >= nsd_expires_at_from($base); } function nsd_stamp_meaningful(array $d, bool $create = false): array { $now = nsd_now(); if ($create || empty($d['created_at'])) $d['created_at'] = (int)($d['created_at'] ?? $now); $d['last_meaningful_activity'] = $now; $d['expires_at'] = nsd_expires_at_from($now); $d['timestamp'] = $now; return $d; } function nsd_crypto_ok(): bool { if (getenv('NSD_FORCE_NO_CRYPTO') === '1') return false; try { $b = random_bytes(16); return is_string($b) && strlen($b) === 16; } catch (Throwable $e) { return false; } } function nsd_import_filter(array $d): bool { return !nsd_is_expired($d); } function nsd_shard_roots() { $o = [nsd_shards()]; foreach (nsd_legacy_bases() as $d) { $s = $d . DIRECTORY_SEPARATOR . 'shards'; if (is_dir($s) && !in_array($s, $o, true)) $o[] = $s; } return $o; } function nsd_glob_shards($rel) { $o = []; foreach (nsd_shard_roots() as $root) { foreach (glob($root . DIRECTORY_SEPARATOR . $rel) ?: [] as $f) $o[] = $f; } return $o; } function nsd_shards() { return nsd_base() . DIRECTORY_SEPARATOR . 'shards'; } function nsd_media_dir() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'media'; if (!is_dir($d)) @mkdir($d, 0755, true); return $d; } /** media/{substr(id,0,2)}/ — 256-way prefix fan-out (durable; not RAM). */ function nsd_media_pref($id) { return substr((string)$id, 0, 2); } function nsd_media_shard_dir($id, $create = true) { $d = nsd_media_dir() . DIRECTORY_SEPARATOR . nsd_media_pref($id); if ($create && !is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_logdir() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'kyc'; if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } function nsd_priv_salt() { $f = nsd_logdir() . DIRECTORY_SEPARATOR . '.salt'; /* A first-hit multiworker race used to mint multiple salts and overwrite the common file. That changed rate-limit/report identities mid-request. */ $fh = @fopen($f, 'c+'); if (!$fh) throw new RuntimeException('Private DATE identity store unavailable'); if (!@flock($fh, LOCK_EX)) { @fclose($fh); throw new RuntimeException('Private DATE identity store locked'); } try { @rewind($fh); $raw = @stream_get_contents($fh, 256); $s = is_string($raw) ? trim($raw) : ''; if (preg_match('/^[a-f0-9]{64}$/D', $s)) return $s; $s = bin2hex(random_bytes(32)); if (!@ftruncate($fh, 0) || @rewind($fh) !== true || @fwrite($fh, $s) !== strlen($s) || !@fflush($fh)) { throw new RuntimeException('Could not persist private DATE identity salt'); } @chmod($f, 0600); return $s; } finally { @flock($fh, LOCK_UN); @fclose($fh); } } function nsd_client_tag($ip, $extra = '') { return substr(hash_hmac('sha256', (string)$ip . '|' . $extra, nsd_priv_salt()), 0, 24); } /** kyc/{salted HMAC}/ day logs - dirent fan-out like chat/work; not RAM. */ function nsd_kyc_ipdir($ip = null) { $ip = ($ip === null || $ip === '') ? (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0') : (string)$ip; $d = nsd_logdir() . DIRECTORY_SEPARATOR . nsd_client_tag($ip, 'log'); if (!is_dir($d)) @mkdir($d, 0700, true); return $d; } function nsd_ratedir() { if (DIRECTORY_SEPARATOR === '/' && is_dir('/dev/shm') && is_writable('/dev/shm')) { $d = '/dev/shm/nosignup_date_ip'; if ((is_dir($d) || @mkdir($d, 0700, true)) && is_writable($d)) return $d; } $d = nsd_base() . DIRECTORY_SEPARATOR . 'ip_posts'; if (!is_dir($d)) @mkdir($d, 0700, true); /* not-ok if returned dir is missing or not writable — ns_worktable_banner reports it */ return $d; } function ns_real_ram_root(): string { return '/dev/shm/nosignup_date_ip'; } function ns_real_ram_ok(): bool { /* REAL RAM only. Disk fallback may still run; the RAM banner still shows. */ if (DIRECTORY_SEPARATOR !== '/' || !is_dir('/dev/shm') || !@is_writable('/dev/shm')) return false; $d = ns_real_ram_root(); if (!is_dir($d)) @mkdir($d, 0700, true); return is_dir($d) && @is_writable($d); } /** Worktable health. REAL /dev/shm root and ./data independently. Fail-open. Loud banners. */ function ns_worktable_problems(): array { $disk = nsd_base(); if ($disk !== '' && !is_dir($disk)) @mkdir($disk, 0755, true); $out = []; if (!ns_real_ram_ok()) $out[] = ['kind' => 'ram', 'msg' => 'No RAM folder. This host has no memory worktable.', 'path' => ns_real_ram_root()]; if (!ns_worktable_folder_ok($disk)) $out[] = ['kind' => 'disk', 'msg' => 'No disk folder. This host cannot save.', 'path' => $disk]; return $out; } function ns_worktable_folder_ok(string $d): bool { return $d !== '' && is_dir($d) && @is_writable($d); } function ns_worktable_banner(bool $public = true): string { $probs = ns_worktable_problems(); if (!$probs) return ''; $lines = ''; foreach ($probs as $p) { $extra = ''; if (!$public && !empty($p['path'])) { $extra = ' ' . htmlspecialchars((string)$p['path'], ENT_QUOTES) . ''; } $lines .= '
' . htmlspecialchars((string)$p['msg'], ENT_QUOTES) . $extra . '
'; } return '' . '
' . $lines . '
'; } function nsd_json($d, $code = 200) { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); header('X-NSD-Backend: file-shard'); echo json_encode($d, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); exit; } function nsd_norm_hash() { $s = (string)@file_get_contents(__FILE__); if (substr($s, 0, 3) === "\xEF\xBB\xBF") $s = substr($s, 3); $s = str_replace(["\r\n", "\r"], "\n", $s); $s = preg_replace('/[ \t]+\n/', "\n", $s); return hash('sha256', rtrim($s, "\n") . "\n"); } /* DATE R43: small, explicit product primitives. No accounts or dependencies. */ function nsd_text_units(string $text): int { if (!preg_match('//u', $text)) return PHP_INT_MAX; preg_match_all('/./us', $text, $chars); $units = 0; foreach ($chars[0] as $ch) $units += strlen($ch) === 4 ? 2 : 1; return $units; } function nsd_field(string $key, int $limit, $default = ''): string { $value = $_POST[$key] ?? $default; if (!is_string($value) || nsd_text_units($value) > $limit) { nsd_json(['ok'=>false,'error'=>"$key must be text, at most $limit characters. Nothing was saved.",'field'=>$key],400); } return trim(str_replace(["\r\n","\r"], "\n", preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value))); } /** R01 A25: reject array/non-scalar request fields before any cast or write * (a cast would turn text[] into the literal message "Array"). */ function nsd_scalar_fields(array $keys): void { foreach ($keys as $key) { foreach ([$_POST, $_GET] as $src) { if (array_key_exists($key, $src) && !is_string($src[$key])) { nsd_json(['ok'=>false,'err'=>"$key must be text. Nothing was saved.",'error'=>"$key must be text. Nothing was saved.",'field'=>$key],400); } } } } function nsd_tags(string $key, int $max, array $default = []): array { if (!array_key_exists($key, $_POST)) return $default; $text = nsd_field($key, 250); $items = preg_split('/[,;\n]+/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: []; $out = []; foreach ($items as $item) { $item = trim($item); if ($item === '') continue; if (nsd_text_units($item) > 24) nsd_json(['ok'=>false,'field'=>$key,'error'=>"Each $key entry must be at most 24 characters."],400); $fold = function_exists('mb_strtolower') ? mb_strtolower($item, 'UTF-8') : strtolower($item); $out[$fold] = $item; } if (count($out) > $max) nsd_json(['ok'=>false,'field'=>$key,'error'=>"Use at most $max $key entries. Nothing was saved."],400); return array_values($out); } /** Fixed 256 lock stripes, not a permanent lock file per visitor. Request lifetime only. */ function nsd_mutation_lock(string $owner): void { if ($owner === '') return; $dir = nsd_base() . '/.mutation-locks'; if (!nsd_mkdir($dir)) nsd_json(['ok'=>false,'error'=>'Private storage unavailable'],503); $f = @fopen($dir.'/'.substr(nsd_owner_hash($owner),0,2).'.lock','c'); if (!$f || !flock($f, LOCK_EX)) nsd_json(['ok'=>false,'error'=>'Could not lock this change. Retry.'],503); nsd_mutation_stripe_held(substr(nsd_owner_hash($owner),0,2), true); register_shutdown_function(static function() use ($f) { @flock($f,LOCK_UN); @fclose($f); }); } function nsd_photo_slot($value): int { if (!is_scalar($value) || !preg_match('/^[012]$/D',(string)$value)) nsd_json(['ok'=>false,'error'=>'Photo slot must be 0, 1 or 2'],400); return (int)$value; } function nsd_photo_stem(string $id, int $slot): string { return $id . ($slot ? '--'.$slot : ''); } function nsd_photo_paths(string $id, int $slot): array { if (!nsd_id_ok($id)) return []; $out = []; $stem = nsd_photo_stem($id,$slot); foreach ([nsd_media_shard_dir($id,false),nsd_media_dir()] as $dir) { foreach (['jpg','jpeg','png','webp','gif'] as $ext) { $p = $dir.'/'.$stem.'.'.$ext; if (is_file($p)) $out[] = $p; } } return $out; } function nsd_photo_delete_slot(string $id, int $slot): bool { $ok = true; foreach (nsd_photo_paths($id,$slot) as $p) if (!@unlink($p) && is_file($p)) $ok = false; return $ok; } function nsd_filter_profile(array $d, array $q): bool { $age = (int)($d['age'] ?? 0); if ($age < max(18,(int)($q['age_min'] ?? 18)) || $age > min(120,(int)($q['age_max'] ?? 120))) return false; foreach (['looking','sex'] as $key) if (!empty($q[$key]) && ($d[$key] ?? '') !== $q[$key]) return false; foreach (['interest'=>'interests','language'=>'languages'] as $query=>$field) { $needle = trim((string)($q[$query] ?? '')); if ($needle !== '' && !preg_match('/'.preg_quote($needle,'/').'/iu',implode(' ',(array)($d[$field] ?? [])))) return false; } $text = trim((string)($q['text'] ?? '')); if ($text !== '') { $haystack = implode(' ',[$d['name']??'', $d['bio']??'', $d['contact']??'', $d['prompt_answer']??'', implode(' ',(array)($d['interests']??[])),implode(' ',(array)($d['languages']??[]))]); if (!preg_match('/'.preg_quote($text,'/').'/iu',$haystack)) return false; } if (!empty($q['photo'])) { $photoId=(string)($d['id']??''); if(!nsd_media_path($photoId,0) && !nsd_media_path($photoId,1) && !nsd_media_path($photoId,2))return false; } return true; } /** Latest bounded tail, used only on an explicit inbox refresh; no read receipts or polling. */ function nsd_thread_tail(string $me, string $peer, ?string $seenKey = null): ?array { $path = nsd_thread_path($me,$peer,false); if (!is_file($path) || @filemtime($path) <= nsd_now()-NSD_EXPIRY_SEC) return null; $f = @fopen($path,'rb'); if (!$f) return null; if (!@flock($f,LOCK_SH)) { fclose($f); return null; } $size = (int)fstat($f)['size']; $start = max(0,$size-8192); fseek($f,$start); $tail = (string)stream_get_contents($f,8192); flock($f,LOCK_UN); fclose($f); $lines = explode("\n",rtrim($tail,"\n")); if ($start > 0) array_shift($lines); $latest = null; $unread = 0; $foundSeen = false; for ($i=count($lines)-1;$i>=0;$i--) { $line=$lines[$i]; $v=json_decode($line,true); if (!is_array($v)) { $p=explode("\t",$line,3); if(count($p)!==3)continue; $v=['ts'=>(int)$p[0],'from'=>$p[1],'text'=>$p[2]]; } if (!isset($v['from'],$v['text']))continue; $key=substr(hash('sha256',$line),0,24); if ($latest===null) $latest=['ts'=>(int)($v['ts']??0),'from_me'=>$v['from']===$me,'key'=>$key,'preview'=>nsd_san((string)$v['text'],96)]; if ($seenKey===null) return $latest; // Thread reads only need the marker. if ($seenKey!=='' && hash_equals($seenKey,$key)) { $foundSeen=true; break; } if ($v['from']!==$me && (int)($v['ts']??0)>nsd_now()-NSD_EXPIRY_SEC) $unread++; } if ($latest!==null && $seenKey!==null) { $latest['unread_count']=$unread; $latest['unread_more']=$start>0 && !$foundSeen; } return $latest; } function nsd_san($s, $m = 2000) { $s = trim((string)($s ?? '')); $s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]+/', ' ', $s); if (!preg_match('//u', $s)) return ''; preg_match_all('/./us', $s, $chars); return implode('', array_slice($chars[0],0,$m)); } /** Tiny purpose brain: bio_risk - soft scam/contact-harvest score; fail open. */ function nsd_brain_bio_risk(string $name, string $bio, string $contact): array { $t = strtolower($name . ' ' . $bio . ' ' . $contact); $hits = []; $bad = [ 'whatsapp' => 0.15, 'telegram.me' => 0.2, 'onlyfans' => 0.1, 'venmo me' => 0.2, 'gift card' => 0.35, 'wire transfer' => 0.3, 'crypto giveaway' => 0.35, 'seed phrase' => 0.5, 'private key' => 0.5, 'send nudes' => 0.25, 'under 18' => 0.9, 'escort rates' => 0.2, 'cashapp' => 0.15, ]; $score = 0.0; foreach ($bad as $k => $w) { if ($k !== '' && str_contains($t, $k)) { $score += $w; $hits[] = $k; } } if ($score > 1.0) $score = 1.0; return ['brain' => 'bio_risk', 'score' => round($score, 3), 'hits' => $hits, 'flag' => $score >= 0.5]; } /** Tiny purpose brain: name_sane - display name length/token soft checks; fail open. */ function nsd_brain_name_sane(string $name): array { $n = trim($name); $notes = []; $score = 0.0; $len = function_exists('mb_strlen') ? mb_strlen($n, 'UTF-8') : strlen($n); if ($len < 1) { $notes[] = 'empty'; $score += 0.5; } if ($len > 40) { $notes[] = 'long'; $score += 0.25; } if (preg_match('/https?:\/\//i', $n) || str_contains(strtolower($n), 'www.')) { $notes[] = 'url'; $score += 0.4; } if (preg_match('/\b(seed|private\s*key|mnemonic)\b/i', $n)) { $notes[] = 'secret'; $score += 0.55; } if (preg_match('/(.)\1{6,}/u', $n)) { $notes[] = 'repeat'; $score += 0.15; } if ($score > 1.0) { $score = 1.0; } return ['brain' => 'name_sane', 'score' => round($score, 3), 'notes' => $notes, 'flag' => $score >= 0.5]; } function nsd_tile_ok($t) { if (!is_string($t) || !preg_match('/^-?\d{1,3}(?:\.5)?_-?\d{1,3}(?:\.5)?$/D', $t)) return false; [$lat, $lon] = array_map('floatval', explode('_', $t)); /* Writes and radius reads must name the same real, canonical half-degree cell. */ return $lat >= -90 && $lat < 90 && $lon >= -180 && $lon < 180 && $t === nsd_half($lat) . '_' . nsd_half($lon); } function nsd_half($v) { $v = floor((float)$v * 2) / 2; return $v == (int)$v ? (string)(int)$v : (string)$v; } function nsd_query_tile_ok($tile) { if (!nsd_tile_ok($tile)) return false; $p = explode('_', $tile); $lat = (float)$p[0]; $lon = (float)$p[1]; return $lat >= -90 && $lat < 90 && $lon >= -180 && $lon < 180; } function nsd_distance_km($lat1, $lon1, $lat2, $lon2) { $a = deg2rad($lat2 - $lat1); $b = deg2rad($lon2 - $lon1); $h = sin($a / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($b / 2) ** 2; return 6371 * 2 * asin(sqrt(max(0, min(1, $h)))); } function nsd_tiles($tile, $r) { if (!nsd_query_tile_ok($tile)) return []; $p = explode('_', $tile); $lat = (float)$p[0] + .25; $lon = (float)$p[1] + .25; $angular = (111 * max(1, min(7, (int)$r))) / 6371; $latSpan = rad2deg($angular); $minLat = max(-90, $lat - $latSpan); $maxLat = min(90, $lat + $latSpan); $lonSpan = ($minLat <= -90 || $maxLat >= 90) ? 180 : rad2deg(asin(min(1, sin($angular) / max(1e-12, cos(deg2rad($lat)))))); $south = (int)floor($minLat * 2); $north = min(179, (int)floor($maxLat * 2)); $west = (int)floor(($lon - $lonSpan) * 2); $east = (int)floor(($lon + $lonSpan) * 2); $out = []; for ($y = $south; $y <= $north; $y++) { $seen = []; for ($x = $west; $x <= $east; $x++) { $wrapped = (($x + 360) % 720 + 720) % 720 - 360; if (isset($seen[$wrapped])) continue; $seen[$wrapped] = true; $out[] = nsd_half($y / 2) . '_' . nsd_half($wrapped / 2); } } return $out; } function nsd_tile_center($tile) { $p = explode('_', $tile); return [(float)$p[0] + .25, (float)$p[1] + .25]; } function nsd_uid($v) { $v = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$v); return (strlen($v) >= 8 && strlen($v) <= 96) ? $v : 'ip'; } function nsd_owner($v) { $v = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$v); return (strlen($v) >= 16 && strlen($v) <= 128) ? $v : ''; } function nsd_owner_hash($owner) { return hash('sha256', 'nsd-owner-v1:' . $owner); } function nsd_id_ok($id) { return is_string($id) && preg_match('/^\d{10}_[a-f0-9]{10,20}$/', $id); } function nsd_new_id($uid, $owner, $ip) { if (!nsd_crypto_ok()) return ''; try { $r = bin2hex(random_bytes(8)); } catch (Throwable $e) { return ''; } return (string)nsd_now() . '_' . substr(hash('sha256', $uid . '|' . $owner . '|' . nsd_client_tag($ip, 'id') . '|' . (string)nsd_now() . '|' . $r), 0, 14); } function nsd_mkdir($p) { if (!is_dir($p)) @mkdir($p, 0755, true); return is_dir($p); } /** Locked IP rate ledger: counting and committing share one exclusive lock. * No client-provided uid can evade the per-IP bucket total. Fail closed on I/O. */ function nsd_rate_atomic($ip, string $bucket, int $max, string $unique = ''): array { $dir = nsd_ratedir(); if (!is_dir($dir) || !is_writable($dir)) return ['wait' => NSD_RATE_WINDOW, 'reason' => 'storage']; $path = $dir . DIRECTORY_SEPARATOR . nsd_client_tag($ip, 'rate') . '.txt'; $f = @fopen($path, 'c+'); if (!$f) return ['wait' => NSD_RATE_WINDOW, 'reason' => 'storage']; if (!@flock($f, LOCK_EX)) { @fclose($f); return ['wait' => NSD_RATE_WINDOW, 'reason' => 'storage']; } $result = ['wait' => NSD_RATE_WINDOW, 'reason' => 'storage']; try { @rewind($f); $raw = stream_get_contents($f, 131072); $all = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($all)) $all = []; $now = time(); $events = is_array($all[$bucket] ?? null) ? $all[$bucket] : []; $events = array_values(array_filter($events, static fn($e) => is_array($e) && is_int($e['time'] ?? null) && ($e['time'] >= $now - NSD_RATE_WINDOW) && is_string($e['unique'] ?? null) )); if ($unique !== '') { foreach ($events as $e) if ($e['unique'] === $unique) { return ['wait' => max(1, $e['time'] + NSD_RATE_WINDOW - $now), 'reason' => 'soon']; } } if (count($events) >= $max) { return ['wait' => max(1, (int)$events[0]['time'] + NSD_RATE_WINDOW - $now), 'reason' => 'busy']; } $events[] = ['time' => $now, 'unique' => $unique]; $all[$bucket] = $events; $bytes = json_encode($all); if ($bytes === false) return $result; if (@ftruncate($f, 0) && @rewind($f) === true) { $written = @fwrite($f, $bytes); if ($written === strlen($bytes) && @fflush($f)) $result = ['wait' => 0, 'reason' => '']; } } finally { @flock($f, LOCK_UN); @fclose($f); } return $result; } function nsd_rate_check($ip, $type, $uid) { return nsd_rate_atomic($ip, 'single_' . preg_replace('/[^a-z0-9_]/', '', (string)$type), NSD_MAX_IDS_PER_IP, (string)$uid); } function nsd_browse_check($ip) { return nsd_rate_atomic($ip, 'browse', NSD_BROWSE_MAX)['wait']; } function nsd_count_rate($ip, $bucket, $max) { return nsd_rate_atomic($ip, 'count_' . preg_replace('/[^a-z0-9_]/', '', (string)$bucket), max(1, (int)$max))['wait']; } function nsd_log($act, $loc) { $ip=(string)($_SERVER['REMOTE_ADDR']??'0.0.0.0'); $dir=nsd_kyc_ipdir($ip); $now=nsd_now(); $path=$dir.DIRECTORY_SEPARATOR.'events.log.txt'; $line=json_encode(['ts'=>$now,'client'=>nsd_client_tag($ip,'event'),'act'=>$act,'loc'=>$loc],JSON_UNESCAPED_SLASHES)."\n"; $f=@fopen($path,'c+b'); if(!$f)return; if(!@flock($f,LOCK_EX)){fclose($f);return;} try { $size=(int)fstat($f)['size']; $stamp=(int)fstat($f)['mtime']; if($stamp <= $now-NSD_EXPIRY_SEC) { ftruncate($f,0); rewind($f); $size=0; } rewind($f); $oldest=json_decode((string)fgets($f,4096),true); if($size+strlen($line)>51200 || (is_array($oldest) && ($oldest['ts']??0)<=$now-NSD_EXPIRY_SEC)) { fseek($f,max(0,$size-32768)); $tail=(string)stream_get_contents($f,32768); if($size>32768){$cut=strpos($tail,"\n");$tail=$cut===false?'':substr($tail,$cut+1);} $kept='';foreach(explode("\n",trim($tail)) as $row){$v=json_decode($row,true);if(is_array($v)&&($v['ts']??0)>$now-NSD_EXPIRY_SEC)$kept.=$row."\n";} rewind($f);ftruncate($f,0);fwrite($f,$kept); } fseek($f,0,SEEK_END);fwrite($f,$line);fflush($f);@chmod($path,0600); } finally {flock($f,LOCK_UN);fclose($f);} /* Old day/overflow files retire under the existing TTL, not copied or multiplied. */ if(mt_rand(1,50)===1)foreach(glob($dir.DIRECTORY_SEPARATOR.'*.log.txt')?:[] as $g)if($g!==$path)nsd_gc_file($g); } /** Resolve media under media/{substr(id,0,2)}/{id}.* then flat back-compat media/{id}.* so pre-migration files still serve. */ function nsd_media_path($id, int $slot = 0) { $paths=nsd_photo_paths((string)$id,$slot); return $paths[0] ?? null; } /** Parent deletion removes every gallery slot, legacy cover included. */ function nsd_media_unlink_all($id) { if (!nsd_id_ok($id)) return; for ($slot=0;$slot<3;$slot++) nsd_photo_delete_slot($id,$slot); } function nsd_delete_profile_files($id, $media = false) { if (!nsd_id_ok($id)) return; foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') as $f) @unlink($f); if ($media) nsd_media_unlink_all($id); } function nsd_find_profile($id) { if (!nsd_id_ok($id)) return null; $files = nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt'); usort($files, function ($a, $b) { $ar = strpos($a, DIRECTORY_SEPARATOR . NSD_REMOTE_TILE . DIRECTORY_SEPARATOR) !== false; $br = strpos($b, DIRECTORY_SEPARATOR . NSD_REMOTE_TILE . DIRECTORY_SEPARATOR) !== false; return ($ar <=> $br); }); foreach ($files as $f) { $raw = @file_get_contents($f); if ($raw === false) continue; $d = json_decode((string)$raw, true); if (!is_array($d)) continue; /* malformed is not verified expired; preserve for recovery */ if (nsd_is_expired($d)) { /* rev3 G2: expiry is decided again on the current record under the owner stripe; a concurrently renewed card is served, never unlinked. Lock failure: not served, not deleted. */ $current = nsd_card_expire_locked($f, $d, $files); if ($current !== null) return [$f, $current]; continue; } return [$f, $d]; } return null; } function nsd_put_profile($d) { $id = $d['id'] ?? ''; $tile = $d['tile'] ?? ''; if (!nsd_id_ok($id) || !nsd_tile_ok($tile)) return false; $json = json_encode($d, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); if ($json === false) return false; $p = nsd_shards() . DIRECTORY_SEPARATOR . $tile; if (!nsd_mkdir($p)) return false; $dst = $p . DIRECTORY_SEPARATOR . $id . '.profile.txt'; /* Not a profile lookup name, so a read will not serve this temp file. */ $part = $p . DIRECTORY_SEPARATOR . $id . '.profile.upload'; /* Identity lock must be independent of destination tile: concurrent map moves must never leave two published copies of the same card. */ $lockDir = nsd_base() . DIRECTORY_SEPARATOR . '.profile-locks'; if (!nsd_mkdir($lockDir)) return false; $lockPath = $lockDir . DIRECTORY_SEPARATOR . substr(hash('sha256', $id),0,2) . '.lock'; // R44: at most 256 lock stripes, not one permanent file per card. $lf = @fopen($lockPath, 'c'); if ($lf === false) return false; if (!@flock($lf, LOCK_EX)) { @fclose($lf); return false; } $ok = false; if (@file_put_contents($part, $json, LOCK_EX) === false) { @unlink($part); } elseif (!@rename($part, $dst)) { @unlink($part); } else { foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') as $f) { if (!is_file($f) || $f === $dst) continue; @unlink($f); } /* No new global remote index; legacy files remain subject to normal expiry/owner edits. */ /* Child media tracks parent meaningful write (read still does not renew). */ $mp = nsd_media_path($id); if ($mp) @touch($mp); $ok = true; } @flock($lf, LOCK_UN); @fclose($lf); return $ok; } function nsd_public_profile($d) { unset($d['owner_hash']); /* Legacy R41 files sometimes contain client-provided GPS-level lat/lon. Only the already-selected half-degree public tile may leave this API. */ if (nsd_tile_ok((string)($d['tile'] ?? ''))) { $center = nsd_tile_center($d['tile']); $d['lat'] = $center[0]; $d['lon'] = $center[1]; $d['city'] = number_format($center[0], 2, '.', '') . ', ' . number_format($center[1], 2, '.', '') . ' (approx. area)'; } $id = $d['id'] ?? ''; unset($d['photo'], $d['photos']); $d['photos'] = []; for ($slot=0;$slot<3;$slot++) { $mp=nsd_media_path($id,$slot); if ($mp) $d['photos'][]=['slot'=>$slot,'url'=>'index.php?api=media&id='.rawurlencode($id).'&slot='.$slot.'&v='.(int)@filemtime($mp)]; } if ($d['photos']) $d['photo']=$d['photos'][0]['url']; $exp = isset($d['expires_at']) ? (int)$d['expires_at'] : nsd_expires_at_from((int)($d['last_meaningful_activity'] ?? $d['timestamp'] ?? nsd_now())); $d['expires_in'] = max(0, $exp - nsd_now()); $d['created_at'] = (int)($d['created_at'] ?? $d['timestamp'] ?? 0); $d['last_meaningful_activity'] = (int)($d['last_meaningful_activity'] ?? $d['timestamp'] ?? 0); $d['expires_at'] = $exp; return $d; } /** Random region-only deck. Filter before pagination; no paid/behavioural ranking. * Reads at most 8,000 candidate files. scan_limited is an explicit subset notice. */ function nsd_list_profiles($tile, $radius, $remote, array $query = [], string $viewer = '', int $offset = 0, string $shuffle = '') { $remote = false; // R44: discovery never widens beyond the selected radius. if ($shuffle === '') $shuffle = hash('sha256',$tile.'|'.$radius.'|'.gmdate('Y-m-d',nsd_now())); $by=[]; $seen=[]; $scanned=0; $limited=false; if (!nsd_query_tile_ok($tile)) return ['profiles'=>[],'total'=>0,'next_offset'=>null,'scan_limited'=>false]; $center=nsd_tile_center($tile); $maxKm=111*max(1,min(7,(int)$radius)); $localTiles=array_fill_keys(nsd_tiles($tile,$radius),true); $paths=(static function() use($localTiles,$remote) { foreach(nsd_shard_roots() as $root) { foreach(array_keys($localTiles) as $t) { $dir=$root.DIRECTORY_SEPARATOR.$t; if (!is_dir($dir))continue; foreach(new DirectoryIterator($dir) as $entry) if($entry->isFile() && str_ends_with($entry->getFilename(),'.profile.txt'))yield $entry->getPathname(); } if (!$remote || !is_dir($root))continue; foreach(new DirectoryIterator($root) as $dir) { if($dir->isDot() || !$dir->isDir() || isset($localTiles[$dir->getFilename()]) || !nsd_tile_ok($dir->getFilename()))continue; foreach(new DirectoryIterator($dir->getPathname()) as $entry) if($entry->isFile() && str_ends_with($entry->getFilename(),'.profile.txt'))yield $entry->getPathname(); } } })(); foreach($paths as $f) { if(++$scanned>8000) { $limited=true; break; } $raw=@file_get_contents($f); if($raw===false)continue; $d=json_decode($raw,true); if(!is_array($d) || !nsd_id_ok((string)($d['id']??'')) || nsd_is_expired($d) || !empty($d['paused']) || !nsd_tile_ok((string)($d['tile']??'')))continue; [$lat,$lon]=nsd_tile_center($d['tile']); $distance=nsd_distance_km($center[0],$center[1],$lat,$lon); if($distance>$maxKm+1e-8 && !($remote && !empty($d['remote'])))continue; if($viewer!=='' && nsd_blocked_pair($viewer,(string)($d['owner_hash']??'')))continue; if(!nsd_filter_profile($d,$query))continue; $id=$d['id']; if(isset($by[$id]) && (int)($by[$id]['timestamp']??0)>(int)($d['timestamp']??0))continue; $d['_distance']=$distance; $d['_shuffle']=hash('sha256',$shuffle.'|'.$id); $by[$id]=$d; } /* A caller-held seed gives a stable random deck across pages; no server session or queue file. */ uasort($by,static fn($a,$b)=>strcmp($a['_shuffle'],$b['_shuffle']) ?: strcmp($a['id'],$b['id'])); $total=count($by); $offset=max(0,$offset); $page=array_slice(array_values($by),$offset,NSD_BROWSE_MAX); $profiles=[]; foreach($page as $d) { unset($d['_distance'],$d['_shuffle']); $profiles[]=nsd_public_profile($d); } return ['profiles'=>$profiles,'total'=>$total,'offset'=>$offset,'next_offset'=>$offset+count($profiles)<$total ? $offset+count($profiles) : null,'scan_limited'=>$limited,'scanned'=>min($scanned,8000)]; } /** Privacy: remove camera/user metadata without needing GD or ImageMagick. * The source is already MIME-checked, capped at 2 MB, and getimagesize-checked. * Reject any container whose boundaries cannot be fully verified. */ function nsd_jpeg_exif_orientation(string $segment): int { if (!str_starts_with($segment, "Exif\0\0")) return 1; $t = substr($segment, 6); $n = strlen($t); if ($n < 14) return 1; $order = substr($t, 0, 2); if ($order !== 'II' && $order !== 'MM') return 1; $u16 = static function (int $pos) use ($t, $n, $order): int { if ($pos < 0 || $pos + 2 > $n) return -1; return unpack($order === 'II' ? 'v' : 'n', substr($t, $pos, 2))[1]; }; $u32 = static function (int $pos) use ($t, $n, $order): int { if ($pos < 0 || $pos + 4 > $n) return -1; return unpack($order === 'II' ? 'V' : 'N', substr($t, $pos, 4))[1]; }; if ($u16(2) !== 42) return 1; $offset = $u32(4); if ($offset < 0 || $offset + 2 > $n) return 1; $entries = $u16($offset); if ($entries < 0 || $entries > 1024 || $offset + 2 + $entries * 12 > $n) return 1; for ($i = 0; $i < $entries; $i++) { $at = $offset + 2 + $i * 12; if ($u16($at) === 0x0112 && $u16($at + 2) === 3 && $u32($at + 4) === 1) { $orientation = $u16($at + 8); return $orientation >= 1 && $orientation <= 8 ? $orientation : 1; } } return 1; } function nsd_image_strip_jpeg(string $s) { $n = strlen($s); if ($n < 4 || substr($s, 0, 2) !== "\xFF\xD8") return false; $out = "\xFF\xD8"; $i = 2; $inScan = false; while ($i < $n) { if ($inScan) { /* A compressed scan may contain FF00-stuffed bytes and restart markers, but a real FFxx denotes a new segment or EOI. This also strips GPS-like data appended after EOI, which an early return at the first SOS used to leave public. */ $start = $i; while ($i < $n) { if (ord($s[$i]) !== 255) { ++$i; continue; } $j = $i + 1; while ($j < $n && ord($s[$j]) === 255) ++$j; if ($j >= $n) return false; $code = ord($s[$j]); if ($code === 0 || ($code >= 0xD0 && $code <= 0xD7)) { $i = $j + 1; continue; } $out .= substr($s, $start, $i - $start); $inScan = false; break; } if ($inScan) return false; continue; } if (ord($s[$i]) !== 255) return false; $start = $i; while ($i < $n && ord($s[$i]) === 255) ++$i; if ($i >= $n) return false; $marker = ord($s[$i++]); if ($marker === 0xD9) return $out . substr($s, $start, $i - $start); if (($marker >= 0xD0 && $marker <= 0xD7) || $marker === 1) { $out .= substr($s, $start, $i - $start); continue; } if ($i + 2 > $n) return false; $size = unpack('n', substr($s, $i, 2))[1]; if ($size < 2 || $i + $size > $n) return false; $seg = substr($s, $i + 2, $size - 2); /* APP0 (JFIF) and APP14 (Adobe CMYK transform) affect decoding; all other APP markers and comments can carry private metadata. */ $isMeta = $marker === 0xFE || ($marker >= 0xE1 && $marker <= 0xEF && $marker !== 0xEE); if (!$isMeta) { $out .= substr($s, $start, $i + $size - $start); } elseif ($marker === 0xE1) { $orient = nsd_jpeg_exif_orientation($seg); if ($orient > 1) { /* Only orientation remains, never location or serial. */ $tag = "Exif\0\0" . "II\x2A\0" . pack('V', 8) . pack('v', 1) . pack('v', 0x0112) . pack('v', 3) . pack('V', 1) . pack('v', $orient) . "\0\0" . pack('V', 0); $out .= "\xFF\xE1" . pack('n', strlen($tag) + 2) . $tag; } } $i += $size; if ($marker === 0xDA) $inScan = true; } return false; } function nsd_image_strip_png(string $s) { $n = strlen($s); if ($n < 20 || substr($s, 0, 8) !== "\x89PNG\r\n\x1A\n") return false; $out = substr($s, 0, 8); $i = 8; $hasEnd = false; $keep = array_fill_keys(['IHDR','PLTE','IDAT','IEND','tRNS','sRGB','gAMA','cHRM','iCCP','pHYs', 'sBIT','bKGD','hIST','sPLT','acTL','fcTL','fdAT'], true); while ($i + 12 <= $n) { $size = unpack('N', substr($s, $i, 4))[1]; $type = substr($s, $i + 4, 4); if ($size > $n - $i - 12) return false; $chunkBytes = $size + 12; if (isset($keep[$type])) $out .= substr($s, $i, $chunkBytes); elseif (ctype_upper($type[0])) return false; /* unknown critical chunk */ $i += $chunkBytes; if ($type === 'IEND') { $hasEnd = true; break; } } return $hasEnd && $i === $n ? $out : false; } function nsd_image_strip_webp(string $s) { $n = strlen($s); if ($n < 12 || substr($s, 0, 4) !== 'RIFF' || substr($s, 8, 4) !== 'WEBP') return false; $declared = unpack('V', substr($s, 4, 4))[1]; if ($declared + 8 !== $n) return false; $out = 'RIFF' . "\0\0\0\0" . 'WEBP'; $i = 12; $vp8xAt = null; $orientation = 1; while ($i + 8 <= $n) { $type = substr($s, $i, 4); $size = unpack('V', substr($s, $i + 4, 4))[1]; $padded = $size + ($size % 2); if ($padded > $n - $i - 8) return false; if ($type === 'EXIF') { $exif = substr($s, $i + 8, $size); /* WebP EXIF may be a bare TIFF (Pillow) or Exif + TIFF. */ if (!str_starts_with($exif, "Exif\0\0")) $exif = "Exif\0\0" . $exif; $v = nsd_jpeg_exif_orientation($exif); if ($v > 1) $orientation = $v; } elseif (in_array($type, ['VP8X','VP8 ','VP8L','ALPH','ANIM','ANMF','ICCP'], true)) { $chunk = substr($s, $i, 8 + $padded); if ($type === 'VP8X' && $size >= 10) { $vp8xAt = strlen($out); $chunk[8] = chr(ord($chunk[8]) & ~0x0C); /* clear EXIF/XMP until rebuilt */ } $out .= $chunk; } $i += 8 + $padded; } if ($i !== $n) return false; if ($orientation > 1 && $vp8xAt !== null) { /* Retain orientation ONLY; no camera make, serial, location, or comments. */ $tiff = "II\x2A\0" . pack('V', 8) . pack('v', 1) . pack('v', 0x0112) . pack('v', 3) . pack('V', 1) . pack('v', $orientation) . "\0\0" . pack('V', 0); $out .= 'EXIF' . pack('V', strlen($tiff)) . $tiff; if (strlen($tiff) % 2) $out .= "\0"; $out[$vp8xAt + 8] = chr(ord($out[$vp8xAt + 8]) | 0x08); } return substr_replace($out, pack('V', strlen($out) - 8), 4, 4); } function nsd_gif_skip_blocks(string $s, int $i) { $n = strlen($s); while ($i < $n) { $size = ord($s[$i++]); if ($size === 0) return $i; if ($size > $n - $i) return false; $i += $size; } return false; } function nsd_image_strip_gif(string $s) { $n = strlen($s); if ($n < 14 || !in_array(substr($s, 0, 6), ['GIF87a','GIF89a'], true)) return false; $packed = ord($s[10]); $gct = ($packed & 0x80) ? 3 * (1 << (($packed & 7) + 1)) : 0; $i = 13 + $gct; if ($i > $n) return false; $out = substr($s, 0, $i); while ($i < $n) { $start = $i; $op = ord($s[$i++]); if ($op === 0x3B) return $i === $n ? $out . "\x3B" : false; if ($op === 0x21) { /* extension */ if ($i + 2 > $n) return false; $type = ord($s[$i++]); $fixed = ord($s[$i++]); if ($fixed > $n - $i) return false; $info = substr($s, $i, $fixed); $i += $fixed; $end = nsd_gif_skip_blocks($s, $i); if ($end === false) return false; $i = $end; /* Keep graphic control (timing/transparency) and animation loop, strip comments/apps/text. */ if ($type === 0xF9 || ($type === 0xFF && $info === 'NETSCAPE2.0')) $out .= substr($s, $start, $i - $start); continue; } if ($op === 0x2C) { /* image descriptor, local color table, compressed pixels */ if ($i + 9 > $n) return false; $packed = ord($s[$i + 8]); $i += 9; if ($packed & 0x80) $i += 3 * (1 << (($packed & 7) + 1)); if ($i + 1 > $n) return false; $i++; /* LZW min code size */ $end = nsd_gif_skip_blocks($s, $i); if ($end === false) return false; $i = $end; $out .= substr($s, $start, $i - $start); continue; } return false; } return false; } function nsd_strip_photo_metadata(string $bytes, string $mime) { if (strlen($bytes) > NSD_MAX_UPLOAD_BYTES) return false; return match ($mime) { 'image/jpeg' => nsd_image_strip_jpeg($bytes), 'image/png' => nsd_image_strip_png($bytes), 'image/webp' => nsd_image_strip_webp($bytes), 'image/gif' => nsd_image_strip_gif($bytes), default => false, }; } function nsd_mime_ext($mime) { return ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'][$mime] ?? ''; } function nsd_image_mime($file) { $h = (string)@file_get_contents($file, false, null, 0, 16); if (substr($h, 0, 8) === "\x89PNG\x0D\x0A\x1A\x0A") return 'image/png'; if (substr($h, 0, 3) === "\xFF\xD8\xFF") return 'image/jpeg'; if (substr($h, 0, 6) === 'GIF87a' || substr($h, 0, 6) === 'GIF89a') return 'image/gif'; if (substr($h, 0, 4) === 'RIFF' && substr($h, 8, 4) === 'WEBP') return 'image/webp'; return ''; } function nsd_mirrorfile() { return nsd_ratedir() . DIRECTORY_SEPARATOR . 'mirrors.txt'; } /** P1-DATE-MIRROR-POISON RR233+RR237 + P1-DATE-MIRROR-LOOPBACK-V6 + P1-DATE-MIRROR-MAPPED-CGNAT: * reject RFC2606/reserved/private + bare/mangled hosts; pure IPs use filter_var * NO_PRIV_RANGE|NO_RES_RANGE so expanded/mapped IPv6 loopback and ULA/link-local * cannot enter mirrors.txt (regex only caught literal ::1 / IPv4 private prefixes). * RFC6598 CGNAT 100.64/10 is not always private in filter_var — reject dotted and * IPv4-mapped/expanded embeddings (::ffff:100.64.0.1, ::ffff:6440:1, full form). */ function nsd_mirror_host_ok($host) { /* PHP parse_url keeps brackets on IPv6 literals — strip before any gate */ $host = strtolower(trim((string)$host, " \t[]")); if (strpos($host, '..') !== false) return false; if (substr($host, -1) === '.') $host = substr($host, 0, -1); if ($host === '') return false; if (preg_match('/[=\s\/\\\\@\[\]]/', $host)) return false; /* pure IP: public only (closes 0:0:…:1, ::ffff:127.0.0.1, fe80::, fc00::/7, …) */ if (filter_var($host, FILTER_VALIDATE_IP)) { /* RFC6598 CGNAT 100.64/10 first — dotted + mapped/expanded embeddings * (filter_var omits CGNAT; NO_RES may also drop ::ffff: on some PHP builds) */ if (nsd_mirror_is_cgnat_v4($host)) return false; /* IPv4-mapped IPv6: judge the embedded v4 explicitly (live PHP can miss ::ffff:127.0.0.1) */ $emb = nsd_mirror_embedded_v4($host); if ($emb !== null && nsd_mirror_is_private_v4_dotted($emb)) return false; if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return false; } return true; } /* hostname path: legacy private-looking prefixes + RFC2606/example */ if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|::1)/i', $host)) return false; /* Dotted "IP-shaped" hosts that are not filter_var IPs (octal 0177.0.0.1, hex, etc.) */ if (nsd_mirror_is_obfuscated_ip_host($host)) return false; if (preg_match('/\.(example|invalid|test|localhost)$/i', $host)) return false; if (preg_match('/^(example|invalid|test|localhost)$/i', $host)) return false; if (preg_match('/(^|\.)example\.(com|net|org|edu)$/i', $host)) return false; /* chat parity: bare label without dot is not a public mirror host */ if (strpos($host, '.') === false) return false; return true; } /** * Reject IP-looking host labels that bypass FILTER_VALIDATE_IP (octal/hex/partial). * P1-DATE-MIRROR-OCTAL-LOOPBACK: 0177.0.0.1 must not store as a "hostname". */ function nsd_mirror_is_obfuscated_ip_host($host) { $host = strtolower(trim((string)$host, " \t[]")); if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; /* Noncanonical numeric forms can be interpreted as IPv4 by a browser. * Require normal dotted-decimal IPs, rather than accepting these as DNS. */ return (bool)preg_match('/^(?:0x[0-9a-f]+|[0-9]+)(?:\.(?:0x[0-9a-f]+|[0-9]+))*\.?$/i', $host); } /** True if $ip is dotted RFC6598 CGNAT or an IPv4-mapped/expanded IPv6 embedding of one. */ function nsd_mirror_is_cgnat_v4($ip) { $ip = strtolower((string)$ip); if (preg_match('/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./', $ip)) return true; $emb = nsd_mirror_embedded_v4($ip); if ($emb !== null && preg_match('/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./', $emb)) return true; return false; } /** Extract dotted IPv4 from ::ffff:0:0/96 mapped forms; null if not mapped. */ function nsd_mirror_embedded_v4($ip) { $ip = strtolower(trim((string)$ip, " \t[]")); $bin = @inet_pton($ip); if ($bin === false || strlen($bin) !== 16) return null; if (strncmp($bin, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 12) !== 0) return null; return ord($bin[12]) . '.' . ord($bin[13]) . '.' . ord($bin[14]) . '.' . ord($bin[15]); } /** * True if dotted IPv4 is loopback/unspecified/RFC1918/link-local/CGNAT. * Used because some PHP builds treat ::ffff:127.0.0.1 as "public" under FILTER_FLAG_NO_PRIV_RANGE. */ function nsd_mirror_is_private_v4_dotted($ip) { $ip = strtolower(trim((string)$ip)); if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; if (preg_match('/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./', $ip)) return true; if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) return true; $long = ip2long($ip); if ($long === false) return true; $u = sprintf('%u', $long); /* 0.0.0.0/8 unspecified, 127.0.0.0/8 loopback (belt if flags ever skip) */ if ($u < 16777216) return true; if ($u >= 2130706432 && $u <= 2147483647) return true; return false; } function nsd_mirror_ok($u) { $u = trim((string)$u); if ($u === '') return null; /* RR form-host parity (chat RR372 / work RR373): strip m|url|host= so body host=nosignup.com is a hostname, not https://host=… */ if (preg_match('/^(?:m|url|host)=/i', $u)) { $u = preg_replace('/^(?:m|url|host)=/i', '', $u); $u = rawurldecode($u); } $u = rawurldecode(trim($u)); if ($u === '' || preg_match('/[=\s]/', $u) && !preg_match('#^https?://#i', $u)) return null; if (!preg_match('#^https?://#i', $u)) $u = 'https://' . $u; if (strlen($u) > 220) return null; /* double-encoded / nested scheme wrap (https://https%3A%2F%2F…) */ if (preg_match('#^https?://https?%3a%2f%2f#i', $u) || preg_match('#^https?://https?://#i', $u)) return null; /* Unbracketed IPv6 / IPv4-mapped authority is ambiguous: parse_url splits the last * :hextet as a port (e.g. ::ffff:6440:1 → host ::ffff:6440 + port 1), then the * truncated host can pass public IP gates. Require [brackets] for multi-colon hosts. */ if (preg_match('~^https?://([^/?#]+)~i', $u, $am)) { $auth = $am[1]; if (strpos($auth, '@') !== false) { $auth = substr($auth, strrpos($auth, '@') + 1); } if ($auth !== '' && $auth[0] !== '[' && substr_count($auth, ':') >= 2) { return null; } } $p = @parse_url($u); if (!$p || empty($p['host'])) return null; if (isset($p['user']) || isset($p['pass'])) return null; $scheme = strtolower($p['scheme'] ?? 'https'); if ($scheme !== 'http' && $scheme !== 'https') return null; /* parse_url may keep [brackets] on IPv6; normalize before gates and storage */ $host = strtolower(trim(rawurldecode($p['host']), " \t[]")); if (strpos($host, '%') !== false) return null; if (!nsd_mirror_host_ok($host)) return null; if (substr($host, -1) === '.') $host = substr($host, 0, -1); /* nested scheme in path/query = mangle / poison wrap */ $rest = ($p['path'] ?? '') . (isset($p['query']) ? '?' . $p['query'] : ''); if (preg_match('#https?://#i', $rest) || preg_match('#https?%3a%2f%2f#i', $rest)) return null; /* P1-DATE-MIRROR-PORT-443-PUBLIC: drop default scheme ports so :443/:80 are not stored as malformed host forms */ $pn = isset($p['port']) ? (int)$p['port'] : 0; $port = ($pn > 0 && !(($scheme === 'https' && $pn === 443) || ($scheme === 'http' && $pn === 80))) ? (':' . $pn) : ''; $path = $p['path'] ?? ''; /* Re-bracket IPv6 literals for a parseable stored URL */ $hostOut = (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) ? ('[' . $host . ']') : $host; return rtrim($scheme . '://' . $hostOut . $port . $path, '/'); } function nsd_mirrors_read() { $f = nsd_mirrorfile(); if (!is_file($f)) return []; $out = []; $now = time(); foreach (file($f, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $p = explode("\t", $line); $u = nsd_mirror_ok($p[0] ?? ''); $t = (int)($p[1] ?? 0); if ($u && $now - $t < NSD_EXPIRY_SEC) $out[$u] = $t; } return $out; } /** @return array{accepted:int,rejected:int,changed:bool} RR340 honest poison accounting */ function nsd_mirrors_add($txt) { $cur = nsd_mirrors_read(); $now = time(); $changed = false; $accepted = 0; $rejected = 0; $nTok = 0; foreach (preg_split('/[\s,]+/', (string)$txt) ?: [] as $raw) { $raw = trim((string)$raw); if ($raw === '') continue; $nTok++; if ($nTok > 32) break; $u = nsd_mirror_ok($raw); if ($u === null) { $rejected++; continue; } $cur[$u] = $now; $changed = true; $accepted++; } if ($changed) { if (count($cur) > 64) $cur = array_slice($cur, -64, null, true); $s = ''; foreach ($cur as $u => $t) $s .= $u . "\t" . $t . "\n"; @file_put_contents(nsd_mirrorfile(), $s, LOCK_EX); } return ['accepted' => $accepted, 'rejected' => $rejected, 'changed' => $changed]; } function nsd_mirror_field_cap($s) { $s = (string)$s; if (strlen($s) > NSD_MIRROR_JSON_BODY_MAX) { nsd_json(['ok' => false, 'err' => 'Body too large'], 413); } return $s; } /** CHAT-2: fail-closed mirror ingest (9.php fun_json_body_read / 4.php nst_device_wallet_read_json_body). Never substr partial JSON. */ function nsd_mirror_body_read() { $declared = trim((string)($_SERVER['CONTENT_LENGTH'] ?? '')); if ($declared !== '') { if (!ctype_digit($declared) || strlen($declared) > 9 || (int)$declared > NSD_MIRROR_JSON_BODY_MAX) { nsd_json(['ok' => false, 'err' => 'Body too large'], 413); } } $fh = @fopen('php://input', 'rb'); if (!is_resource($fh)) { return ''; } $raw = @stream_get_contents($fh, NSD_MIRROR_JSON_BODY_MAX + 1); @fclose($fh); if (!is_string($raw)) { return ''; } if (strlen($raw) > NSD_MIRROR_JSON_BODY_MAX) { nsd_json(['ok' => false, 'err' => 'Body too large'], 413); } return $raw; } function nsd_mirrors_sample($n) { $u = array_keys(nsd_mirrors_read()); if (!$u) return []; shuffle($u); return array_slice($u, 0, $n); } /* ---- DATE 30-day forget helpers (wrap existing filemtime / mt_rand ethos) ---- */ /** Unlink one file if older than NSD_EXPIRY_SEC. Returns true if gone/expired. */ function nsd_gc_file($path) { if (!is_string($path) || $path === '' || !is_file($path)) return true; if (@filemtime($path) <= nsd_now() - NSD_EXPIRY_SEC) { @unlink($path); return true; } return false; } /** * Cascade when a profile expires or is deleted: media + owner index + reverse Keeps. * Threads keep their own pair inactivity law (filemtime); they store no full profile snapshot. */ function nsd_profile_forget_cascade($id, $owner_hash = '') { if (nsd_id_ok($id)) nsd_media_unlink_all($id); if ($owner_hash !== '' && preg_match('/^[a-f0-9]{64}$/', $owner_hash)) { /* Callers (admin remove_card) have verified that no card file remains. */ nsd_owner_cascade_if_current($owner_hash, (string)$id, true, [], nsd_owner_stripe_held($owner_hash)); /* admin path: no owner stripe => defer */ } } /** * STORAGE/EXPIRY CONTRACT (U1, EC-03): owners//.owner.txt names the * owner's CURRENT card; matches/// is that owner's Keeps inbox. * rev3: Keeps are stored per index generation (see nsd_owner_index_state). * When card $id expires or is removed, only the index is cleared (logical), and * only if the index still names $id and the current card record is gone or * expired. Physical Keeps removal happens on the owner's own touch * (nsd_owner_reap), bounded per request. Unreadable or malformed => defer. * Runs under the owner's .mutation-locks stripe, the lock post_profile and * other owner writes hold while replacing the index (reused if held). * Cleanup takes the stripe with ONE LOCK_NB try; if it is busy, cleanup waits for a later touch. * Expiry on read is unchanged: expired cards are still never served. * A malformed index (first line not a card id) is unlinked by * nsd_owner_index_gc_malformed under the same stripe, only if a re-read under * the lock shows it is STILL malformed and STILL past NSD_EXPIRY_SEC. */ function nsd_mutation_stripe_held(string $stripe, bool $mark = false): bool { static $held = []; if ($mark) $held[$stripe] = true; return isset($held[$stripe]); } /** Owner stripe for NON-cleanup writers (Keep writes): null = this request already holds it, * resource = acquired here (caller releases), false = busy ~200 ms / unavailable. Cleanup uses nsd_owner_stripe_try. */ function nsd_owner_stripe_acquire(string $owner_hash) { $stripe = substr($owner_hash, 0, 2); if (nsd_mutation_stripe_held($stripe)) return null; $dir = nsd_base() . '/.mutation-locks'; if (!nsd_mkdir($dir)) return false; $lf = @fopen($dir . '/' . $stripe . '.lock', 'c'); if (!$lf) return false; for ($try = 0; !@flock($lf, LOCK_EX | LOCK_NB); $try++) { if ($try >= 20) { @fclose($lf); return false; } usleep(10000); } return $lf; } /** Bounded GC inside one directory: unlink expired matches of $glob, cap $limit. */ function nsd_gc_dir_expired($dir, $glob, $limit = 48) { if (!is_dir($dir)) return 0; $n = 0; $killed = 0; foreach (glob($dir . DIRECTORY_SEPARATOR . $glob) ?: [] as $f) { if (!is_file($f)) continue; if (nsd_gc_file($f)) { $killed++; } if (++$n >= $limit) break; } return $killed; } /** Pick one random prefix shard under $root and GC expired files (bounded). */ function nsd_gc_random_prefix($root, $glob, $limit = 48) { if (!is_dir($root)) return; $prefs = glob($root . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: []; if (!$prefs) { nsd_gc_dir_expired($root, $glob, $limit); return; } nsd_gc_dir_expired($prefs[array_rand($prefs)], $glob, $limit); } /** Read-only: parent card still inside NSD expiry. Does not stamp, unlink, or touch media mtime. */ function nsd_profile_is_live($id) { if (!nsd_id_ok($id)) return false; $unreadable = false; foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') as $f) { $raw = @file_get_contents($f); /* A failed read is not expiry and not absence. Callers must not age-delete on it. */ if ($raw === false) { $unreadable = true; continue; } $d = json_decode((string)$raw, true); if (is_array($d) && !nsd_is_expired($d)) return true; } if ($unreadable) return true; return false; } /** Age-delete one media file only when its parent profile is not live. Same NSD_EXPIRY_SEC filemtime law. */ function nsd_gc_media_file($path) { if (!is_string($path) || $path === '' || !is_file($path)) return true; $id = preg_replace('/--[12]$/D','',pathinfo($path, PATHINFO_FILENAME)); if (nsd_id_ok($id)) { foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') as $f) { $raw = @file_get_contents($f); if ($raw === false) return false; $d = json_decode((string)$raw, true); if (is_array($d) && !nsd_is_expired($d)) return false; } } return nsd_gc_file($path); } function nsd_gc_media_dir_expired($dir, $limit = 48) { if (!is_dir($dir)) return 0; $n = 0; $killed = 0; foreach (glob($dir . DIRECTORY_SEPARATOR . '*.*') ?: [] as $f) { if (!is_file($f)) continue; if (nsd_gc_media_file($f)) { $killed++; } if (++$n >= $limit) break; } return $killed; } /** One random media shard. A live parent's photo is not deleted for file age. */ function nsd_gc_media_random($limit = 48) { $root = nsd_media_dir(); if (!is_dir($root)) return; $prefs = glob($root . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: []; if (!$prefs) { nsd_gc_media_dir_expired($root, $limit); return; } nsd_gc_media_dir_expired($prefs[array_rand($prefs)], $limit); } /** Age-delete one owner index only when its profile is not live. Same NSD_EXPIRY_SEC filemtime law. */ function nsd_gc_owner_file($path) { if (!is_string($path) || $path === '' || !is_file($path)) return true; $h = @fopen($path, 'rb'); $raw = $h ? @fread($h, NSD_IDX_CAP + 1) : false; if ($h) @fclose($h); if ($raw === false) return false; $id = strlen($raw) > NSD_IDX_CAP ? '' : trim(explode("\n", $raw)[0]); if (nsd_id_ok($id) && nsd_profile_is_live($id)) return false; $oh = basename($path, '.owner.txt'); if (!nsd_id_ok($id)) { return preg_match('/^[a-f0-9]{64}$/', $oh) ? nsd_owner_index_gc_malformed($oh) : nsd_gc_file($path); } if (@filemtime($path) > nsd_now() - NSD_EXPIRY_SEC) return false; nsd_owner_cascade_if_current($oh, $id, false); clearstatcache(true, $path); return !is_file($path); } function nsd_gc_owner_dir_expired($dir, $limit = 48) { if (!is_dir($dir)) return 0; $n = 0; $killed = 0; foreach (glob($dir . DIRECTORY_SEPARATOR . '*.owner.txt') ?: [] as $f) { if (!is_file($f)) continue; if (nsd_gc_owner_file($f)) { $killed++; } if (++$n >= $limit) break; } return $killed; } /** One random owners shard. A live profile's index is not deleted for file age. */ function nsd_gc_owners_random($limit = 48) { $root = nsd_owners_root(); if (!is_dir($root)) return; $prefs = glob($root . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: []; if (!$prefs) { nsd_gc_owner_dir_expired($root, $limit); return; } nsd_gc_owner_dir_expired($prefs[array_rand($prefs)], $limit); } /** * Bounded lazy DATE purge — no cron. Guard file limits global work to ~once / 15 min * when traffic exists. Prefer shard-local GC on touch; this only sweeps cold shards. */ /* rev4 narrow retirement of background triggers. Each switch is separable; revert = set to true. Exact-touch expiry replaces them (rev4 RECEIPT §5). Report cleanup (nsd_report_gc_bounded) is NOT governed here and stays as is (separate, non-compliant random debt). */ const NSD_BG_GC_OWNERS = false; /* random owners-index GC (valid stale indexes then defer: no exact proof) */ const NSD_BG_GC_THREADS = false; /* random threads GC */ const NSD_BG_GC_MEDIA = false; /* random media GC */ function nsd_purge_expired() { if (!NSD_BG_GC_OWNERS && !NSD_BG_GC_THREADS && !NSD_BG_GC_MEDIA) return; $guard = nsd_base() . DIRECTORY_SEPARATOR . '.last_date_purge'; $now = time(); if (is_file($guard) && @filemtime($guard) > $now - 900) return; $fh = @fopen($guard, 'c+'); if ($fh === false) return; if (!@flock($fh, LOCK_EX | LOCK_NB)) { fclose($fh); return; } clearstatcache(true, $guard); if (@filemtime($guard) > $now - 900) { flock($fh, LOCK_UN); fclose($fh); return; } @ftruncate($fh, 0); @fwrite($fh, (string)$now); @fflush($fh); /* rev3 retired the cold sweep and the random Keeps GC; rev4 switches above gate the rest. */ if (NSD_BG_GC_OWNERS && mt_rand(1, 3) === 1) nsd_gc_owners_random(48); if (NSD_BG_GC_THREADS && mt_rand(1, 3) === 1) nsd_gc_random_prefix(nsd_threads_root(), '*.txt', 48); if (NSD_BG_GC_MEDIA && mt_rand(1, 3) === 1) nsd_gc_media_random(48); flock($fh, LOCK_UN); fclose($fh); } /* ======================================================================== * WHO-KEPT-YOU + PAIR THREADS (DATE-MATCH) * Identity = browser-owned nsd_owner key (hashed server-side). No accounts. * Semantics (operator law — implement exactly): * KEEP A→B = record so B's Who-Kept-you list lists A; A still cannot message B. * WHO-KEPT = people who Kept ME (actionable message-gates only). * SEND msg = append pair .txt + auto-Keep me→them (two-way if both). * Storage lives under nsd_base() (./data, or existing /var/lib/nosignup/date / .nsd-data), same TTL as cards. * filemtime + NSD_EXPIRY_SEC; reads do not renew. Inbox stays; 30d inactivity forgets. * Not a ranking engine, not a global social graph, not money. * ======================================================================== */ function nsd_matches_root($create = true) { $d = nsd_base() . DIRECTORY_SEPARATOR . 'matches'; if ($create && !is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_threads_root($create = true) { $d = nsd_base() . DIRECTORY_SEPARATOR . 'threads'; if ($create && !is_dir($d)) @mkdir($d, 0755, true); return $d; } function nsd_owners_root() { $d = nsd_base() . DIRECTORY_SEPARATOR . 'owners'; if (!is_dir($d)) @mkdir($d, 0755, true); return $d; } /** 256-way prefix from a 64-hex owner hash (or any long id). */ function nsd_hash_pref($h) { return substr((string)$h, 0, 2); } /** Durable reverse index: owner_hash → latest profile id (for Who-Kept-you cards). */ function nsd_owner_index_path($owner_hash) { $pref = nsd_hash_pref($owner_hash); $dir = nsd_owners_root() . DIRECTORY_SEPARATOR . $pref; if (!is_dir($dir)) @mkdir($dir, 0755, true); return $dir . DIRECTORY_SEPARATOR . $owner_hash . '.owner.txt'; } /** Atomic file replacement with unique staging; no caller may reuse a fixed .upload filename. */ function nsd_atomic_replace(string $path, string $bytes): bool { $dir = dirname($path); if (!nsd_mkdir($dir)) return false; $tmp = @tempnam($dir, '.nsd-'); if ($tmp === false) return false; $ok = @file_put_contents($tmp, $bytes, LOCK_EX) !== false && @rename($tmp, $path); if (!$ok) @unlink($tmp); return $ok; } function nsd_owner_index_clear($owner_hash) { if ($owner_hash === '') return; $p = nsd_owner_index_path($owner_hash); if (is_file($p)) @unlink($p); } /* ---- rev3: owner index generations + bounded on-touch Keeps reclamation ---------------------- * Index file owners//.owner.txt: * legacy "id\ntime" (exactly 2 lines; a read also tolerates one trailing "\n") => generation '' * current "id\ntime\ngen" (gen = 16 lowercase hex) => generation gen * Keeps of generation '' live in matches///; of gen G in matches///g-G/. * Readers see only the generation named by the index, and only while the index names a live card. * Reads are bounded (open + fstat + fread <= NSD_IDX_CAP+1 bytes). "absent" means ENOENT only; * any other failure, oversize or malformed content means DEFER: no deletion, no fallback, no write. */ const NSD_IDX_CAP = 256; const NSD_RB_ENTRIES = 64; /* readdir calls per request, incl. '.', '..', unknown names, final false */ const NSD_RB_UNLINKS = 32; /* unlink attempts per request, failed ones included */ const NSD_RB_RMDIRS = 4; /* rmdir attempts per request */ const NSD_RB_DIRS = 12; /* opendir + lstat attempts per request */ const NSD_RB_BYTES = 4096; /* index bytes read by cleanup per request */ const NSD_RB_LOCKS = 8; /* new stripe acquisitions by cleanup per request (one LOCK_NB try each) */ const NSD_RB_RECORDS = 4; /* card copies read under a lock per request (each capped at NSD_CARD_CAP) */ const NSD_RB_STATS = 64; /* is_file / stat probes by cleanup per request (media slot paths) */ const NSD_CARD_CAP = 65536; /* bytes read per card copy by cleanup; larger => unknown => defer */ const NSD_RB_MS = 20; /* cooperative wall budget: checked before every counted op */ /** One request-wide cleanup budget. Returns false (and records why) once any cap or the clock is spent. */ function nsd_rb_take(string $k, int $n = 1): bool { static $caps = ['entries' => NSD_RB_ENTRIES, 'unlinks' => NSD_RB_UNLINKS, 'rmdirs' => NSD_RB_RMDIRS, 'dirs' => NSD_RB_DIRS, 'bytes' => NSD_RB_BYTES, 'locks' => NSD_RB_LOCKS, 'records' => NSD_RB_RECORDS, 'stats' => NSD_RB_STATS]; $b = &nsd_rb_state(); if ($b['t0'] === null) $b['t0'] = hrtime(true); if ((hrtime(true) - $b['t0']) / 1e6 >= NSD_RB_MS) { $b['stopped'] = 'time'; return false; } if ($b[$k] + $n > $caps[$k]) { $b['stopped'] = $k; return false; } $b[$k] += $n; return true; } function &nsd_rb_state(): array { static $b = ['entries' => 0, 'unlinks' => 0, 'rmdirs' => 0, 'dirs' => 0, 'bytes' => 0, 'locks' => 0, 'records' => 0, 'stats' => 0, 't0' => null, 'stopped' => null]; return $b; } /** Re-entrant owner stripe. True = held (by this request's mutation lock, an outer frame, or acquired * now); false = busy (cleanup: one LOCK_NB try; writers: ~200 ms) or unavailable, or (cleanup only) the lock budget is spent. * Keep writes pass $cleanup = false: they are not cleanup and never depend on the cleanup budget. */ function nsd_stripe_enter(string $owner_hash, bool $cleanup = true): bool { $stripe = substr($owner_hash, 0, 2); $own = &nsd_stripe_own(); if (nsd_mutation_stripe_held($stripe)) return true; if (isset($own[$stripe])) { $own[$stripe][1]++; return true; } if ($cleanup && !nsd_rb_take('locks')) return false; $lf = $cleanup ? nsd_owner_stripe_try($owner_hash) : nsd_owner_stripe_acquire($owner_hash); if ($lf === false) return false; if ($lf === null) return true; $own[$stripe] = [$lf, 1]; return true; } function nsd_stripe_leave(string $owner_hash): void { $stripe = substr($owner_hash, 0, 2); $own = &nsd_stripe_own(); if (!isset($own[$stripe])) return; if (--$own[$stripe][1] > 0) return; @flock($own[$stripe][0], LOCK_UN); @fclose($own[$stripe][0]); unset($own[$stripe]); } function &nsd_stripe_own(): array { static $own = []; return $own; } /** Cleanup acquisition: a single LOCK_NB attempt (no waiting); busy => false => the caller defers. */ function nsd_owner_stripe_try(string $owner_hash) { $dir = nsd_base() . '/.mutation-locks'; if (!nsd_mkdir($dir)) return false; $lf = @fopen($dir . '/' . substr($owner_hash, 0, 2) . '.lock', 'c'); if (!$lf) return false; if (!@flock($lf, LOCK_EX | LOCK_NB)) { @fclose($lf); return false; } return $lf; } /** Owner index state. ['s'=>'ok','id','gen','mtime'] | ['s'=>'absent'] | ['s'=>'defer','why','malformed'?]. */ function nsd_owner_index_state(string $owner_hash, bool $budgeted = false): array { if (!preg_match('/^[a-f0-9]{64}$/', $owner_hash)) return ['s' => 'defer', 'why' => 'owner']; $p = nsd_owner_index_path($owner_hash); clearstatcache(true, $p); if ($budgeted && !nsd_rb_take('bytes', NSD_IDX_CAP + 1)) return ['s' => 'defer', 'why' => 'budget']; error_clear_last(); $h = @fopen($p, 'rb'); if (!$h) { $e = (string)(error_get_last()['message'] ?? ''); return stripos($e, 'No such file') !== false ? ['s' => 'absent'] : ['s' => 'defer', 'why' => 'open']; } $st = @fstat($h); $raw = @fread($h, NSD_IDX_CAP + 1); @fclose($h); if ($st === false || $raw === false) return ['s' => 'defer', 'why' => 'read']; $bad = ['s' => 'defer', 'why' => 'malformed', 'malformed' => true, 'mtime' => (int)$st['mtime']]; if (strlen($raw) > NSD_IDX_CAP) return $bad; $l = explode("\n", $raw); if (!nsd_id_ok($l[0]) || !isset($l[1]) || !ctype_digit($l[1])) return $bad; if (count($l) === 2 || (count($l) === 3 && $l[2] === '')) $gen = ''; elseif (count($l) === 3 && preg_match('/^[a-f0-9]{16}$/D', $l[2])) $gen = $l[2]; else return $bad; return ['s' => 'ok', 'id' => $l[0], 'gen' => $gen, 'mtime' => (int)$st['mtime']]; } /** Callers hold the owner's stripe (post_profile mutation lock; renew runs inside it). * Same card: generation carried; legacy '' stays the exact 2-line form. * Different card or no index: fresh generation (old-incarnation Keeps never resurrect). * Unreadable or malformed index: no write (defer), never a fresh generation. */ function nsd_owner_index_put($owner_hash, $profile_id) { if ($owner_hash === '' || !nsd_id_ok($profile_id)) return false; $s = nsd_owner_index_state((string)$owner_hash); if ($s['s'] === 'defer') return false; if ($s['s'] === 'ok' && $s['id'] === $profile_id) { $gen = $s['gen']; } else { try { $gen = bin2hex(random_bytes(8)); } catch (Throwable $e) { return false; } } $bytes = $profile_id . "\n" . (string)time() . ($gen === '' ? '' : "\n" . $gen); return nsd_atomic_replace(nsd_owner_index_path($owner_hash), $bytes); } function nsd_owner_index_get($owner_hash) { if ($owner_hash === '' || !preg_match('/^[a-f0-9]{64}$/', $owner_hash)) return null; $s = nsd_owner_index_state($owner_hash); if ($s['s'] === 'absent') return null; if ($s['s'] === 'defer') { if (!empty($s['malformed'])) nsd_owner_index_gc_malformed($owner_hash); return null; } $id = $s['id']; /* Live parent keeps this index past file age. Read does not rewrite or touch mtime. */ if (nsd_profile_is_live($id)) return $id; if ($s['mtime'] > nsd_now() - NSD_EXPIRY_SEC) return $id; /* Stale index: the card read expires it through exact paths (find_profile); with no copy left at all the index is cleared with an empty exact proof. */ $held = nsd_owner_stripe_held($owner_hash); /* rev6: was the stripe held while the proof is collected? */ if (nsd_find_profile($id) === null && !nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt')) { nsd_owner_cascade_if_current($owner_hash, $id, false, [], $held); } return null; } /** Unlink a malformed owner index only if, under the stripe, it is still malformed and expired. */ function nsd_owner_index_gc_malformed(string $owner_hash): bool { if (!preg_match('/^[a-f0-9]{64}$/', $owner_hash)) return false; if (!nsd_stripe_enter($owner_hash)) return false; try { $s = nsd_owner_index_state($owner_hash, true); if ($s['s'] === 'absent') return true; if (empty($s['malformed'])) return false; if ($s['mtime'] > nsd_now() - NSD_EXPIRY_SEC) return false; if (!nsd_rb_take('unlinks')) return false; /* rev5: the attempt is charged even if the unlink fails */ $p = nsd_owner_index_path($owner_hash); @unlink($p); clearstatcache(true, $p); return !is_file($p); } finally { nsd_stripe_leave($owner_hash); } } /** Current card record for $id: 'live' | 'gone' (no file, or every copy expired) | 'unknown' (unreadable, * malformed or another owner's record: never treated as expired). */ /** One card copy at an EXACT path, read with a byte cap (budgeted). Revalidates id AND owner. * ['s'=>'live'|'expired', 'd'=>array] | ['s'=>'gone'] (ENOENT) | ['s'=>'unknown','why'] (anything else). */ function nsd_card_at(string $f, string $id, string $owner_hash): array { if (!nsd_rb_take('records')) return ['s' => 'unknown', 'why' => 'budget']; error_clear_last(); $h = @fopen($f, 'rb'); if (!$h) { $e = (string)(error_get_last()['message'] ?? ''); return stripos($e, 'No such file') !== false ? ['s' => 'gone'] : ['s' => 'unknown', 'why' => 'open']; } $raw = @fread($h, NSD_CARD_CAP + 1); @fclose($h); if ($raw === false) return ['s' => 'unknown', 'why' => 'read']; if (strlen($raw) > NSD_CARD_CAP) return ['s' => 'unknown', 'why' => 'oversize']; $d = json_decode($raw, true); if (!is_array($d) || (string)($d['id'] ?? '') !== $id || (string)($d['owner_hash'] ?? '') !== $owner_hash) { return ['s' => 'unknown', 'why' => 'identity']; } return ['s' => nsd_is_expired($d) ? 'expired' : 'live', 'd' => $d]; } /** * LOGICAL expiry/removal of an owner's card $id: clear the index under the owner's stripe, only if the * index still names $id AND the current card record is gone or expired (a concurrent renewal => no-op). * O(1); never reaps Keeps physically, because it is also reached from legacy random GC (nsd_gc_owner_file). * $withInbox is kept for call-site compatibility: Keeps become invisible with the index (readers show * only the indexed generation of a live card); physical removal happens in nsd_owner_reap on touch. */ /** * LOGICAL expiry/removal of an owner's card $id: clear the index under the owner's stripe, only if the * index still names $id AND every card copy at the caller's EXACT paths is gone or expired (id and owner * revalidated, capped reads). $paths === null means "no exact proof" => defer; [] means the caller has * just confirmed no copy exists (e.g. after a verified delete). Work under the lock: one index read, * <= NSD_RB_RECORDS capped record reads, one unlink, all budgeted; nothing is enumerated. Never reaps. * Readers never depend on this succeeding: they gate on a live card (nsd_live_inbox_dir, find_profile). */ /** rev6: true if THIS request already holds the owner's stripe (its mutation lock or an outer cleanup frame). */ function nsd_owner_stripe_held(string $owner_hash): bool { $stripe = substr($owner_hash, 0, 2); $own = &nsd_stripe_own(); return nsd_mutation_stripe_held($stripe) || isset($own[$stripe]); } function nsd_owner_cascade_if_current(string $owner_hash, string $id, bool $withInbox = true, ?array $paths = null, bool $proofUnderStripe = false): void { if ($paths === null || count($paths) > NSD_RB_RECORDS) return; if (!preg_match('/^[a-f0-9]{64}$/', $owner_hash) || !nsd_id_ok($id)) return; if (!nsd_stripe_enter($owner_hash)) return; try { /* rev6: an empty proof collected BEFORE the stripe is UNKNOWN (a same-id publish may have landed): defer. */ if ($paths === [] && !$proofUnderStripe) return; foreach ($paths as $p) { $r = nsd_card_at((string)$p, $id, $owner_hash); if ($r['s'] === 'live' || $r['s'] === 'unknown') return; } nsd_owner_index_clear_if_names($owner_hash, $id); } finally { nsd_stripe_leave($owner_hash); } } /** Caller holds the owner's stripe and has proved the card $id gone/expired. One read, one unlink. */ function nsd_owner_index_clear_if_names(string $owner_hash, string $id): void { $s = nsd_owner_index_state($owner_hash, true); if ($s['s'] !== 'ok' || $s['id'] !== $id) return; if (!nsd_rb_take('unlinks')) return; @unlink(nsd_owner_index_path($owner_hash)); } /** delete_profile: LOGICAL removal of the owner link for $id (not a cleanup step, so not budget-gated). * Leaves an index that names ANOTHER card alone; removes one naming $id, or a malformed/legacy-unreadable one. */ function nsd_delete_index_logical(string $owner_hash, string $id): void { if ($owner_hash === '') return; $s = nsd_owner_index_state($owner_hash); if ($s['s'] === 'ok' && $s['id'] !== $id) return; if ($s['s'] === 'absent') return; @unlink(nsd_owner_index_path($owner_hash)); } /** * find_profile saw $d (read from $f) expired. Revalidate under the owner's stripe and only then remove * media, index and card file. Returns the CURRENT record if a concurrent renewal made it live again, * else null. If the stripe cannot be taken the card is still not served (logical expiry), nothing is * deleted, and a later touch retries. */ /** * find_profile saw $d (read from $f) expired. $files are the exact copies find_profile already resolved. * Under the owner's stripe: capped re-read of $f (id + owner revalidated); a concurrent renewal is served. * Physical cleanup (media of the 3 slots, index, card file) runs only if the whole proof and every * removal fit the request budget up front; otherwise it is DEFERRED to a later touch. The card is never * served once expired, whatever happens to cleanup (logical expiry). */ function nsd_card_expire_locked(string $f, array $d, array $files = []): ?array { $id = (string)($d['id'] ?? ''); $oh = (string)($d['owner_hash'] ?? ''); if (!nsd_id_ok($id) || !preg_match('/^[a-f0-9]{64}$/', $oh)) return null; /* no owner: hide, defer */ if (count($files) > NSD_RB_RECORDS) return null; /* proof too large: defer */ if (!nsd_stripe_enter($oh)) return null; try { $r = nsd_card_at($f, $id, $oh); if ($r['s'] === 'live') return $r['d']; if ($r['s'] !== 'expired') return null; foreach ($files as $g) { if ($g === $f) continue; $o = nsd_card_at((string)$g, $id, $oh); if ($o['s'] === 'live' || $o['s'] === 'unknown') return null; /* another copy decides; no cleanup */ } if (!nsd_rb_take('stats', 30)) return null; /* 3 slots x 5 ext x 2 dirs probes */ $media = []; for ($slot = 0; $slot < 3; $slot++) foreach (nsd_photo_paths($id, $slot) as $p) $media[] = $p; if (!nsd_rb_take('unlinks', count($media) + 1)) return null; /* media + card, reserved up front */ foreach ($media as $p) @unlink($p); nsd_owner_index_clear_if_names($oh, $id); @unlink($f); return null; } finally { nsd_stripe_leave($oh); } } /** Keeps root for an owner (generation '' lives here; generation G in g-G/). Never created here. */ function nsd_match_root_dir(string $owner_hash): string { return nsd_matches_root(false) . DIRECTORY_SEPARATOR . nsd_hash_pref($owner_hash) . DIRECTORY_SEPARATOR . $owner_hash; } function nsd_match_gen_dir(string $owner_hash, string $gen): string { return nsd_match_root_dir($owner_hash) . ($gen === '' ? '' : DIRECTORY_SEPARATOR . 'g-' . $gen); } /** Directory of Keep records targeting $to_owner_hash in its CURRENT generation. Without a readable * index this is a path that never exists, so nothing is visible. Writes go through nsd_keep_write. */ /** Keeps visible to readers (has_match, thread touch): only the indexed generation of a LIVE card; else a * path that never exists. Same rule as nsd_live_inbox_dir; logical expiry does not depend on any lock. */ function nsd_match_to_dir($to_owner_hash, $create = false) { $d = nsd_live_inbox_dir((string)$to_owner_hash); return $d ?? nsd_match_root_dir((string)$to_owner_hash) . DIRECTORY_SEPARATOR . 'g-none'; } /** Removals (block, revoke, delete-confirm) target the generation the index names, live or not, so a * just-expired card's stored Keep is still removed when asked. Empty string = no readable index. */ function nsd_match_current_dir(string $owner_hash): string { $s = nsd_owner_index_state($owner_hash); return $s['s'] === 'ok' ? nsd_match_gen_dir($owner_hash, $s['gen']) : ''; } /** Keep write bound to the recipient incarnation: under the RECIPIENT stripe its index must still name * $to_profile_id. Returns 'ok' | 'gone' (index absent or names another card) | 'busy' (lock/read) | 'fail'. */ function nsd_keep_write(string $to_oh, string $from_oh, string $to_profile_id, string $json): string { if (!nsd_stripe_enter($to_oh, false)) return 'busy'; try { $s = nsd_owner_index_state($to_oh); if ($s['s'] === 'defer') return 'busy'; if ($s['s'] === 'absent' || $s['id'] !== $to_profile_id) return 'gone'; $dir = nsd_match_gen_dir($to_oh, $s['gen']); if (!nsd_mkdir($dir)) return 'fail'; return nsd_atomic_replace($dir . DIRECTORY_SEPARATOR . $from_oh . '.match.txt', $json) ? 'ok' : 'fail'; } finally { nsd_stripe_leave($to_oh); } } /** The owner's Keeps are actionable only while their indexed card is live (incarnation gate). */ function nsd_live_inbox_dir(string $owner_hash): ?string { $mine = nsd_live_owner_profile($owner_hash); if ($mine === null) return null; $s = nsd_owner_index_state($owner_hash); if ($s['s'] !== 'ok' || $s['id'] !== $mine['id']) return null; return nsd_match_gen_dir($owner_hash, $s['gen']); } /** delete_profile (requesting owner): one bounded on-touch reap; the rest is left to later touches. */ function nsd_match_inbox_wipe($owner_hash) { nsd_owner_reap((string)$owner_hash); } /** * Bounded physical reclamation for ONE owner, run only on that owner's own request paths * (nsd_live_owner_profile, blackbook, delete_profile), never from purge/GC. Under the owner's stripe, * with a current index state; defers on any failure. Deletes only documented shapes: * root: <64hex>.match.txt (legacy generation '') when the current generation is not '' * stale dirs: g-<16hex> other than the current one: <64hex>.match.txt and .nsd-* staging files, * then rmdir if the listing completed with no unknown names left. * Symlinks are never followed (root or g- dir that is a link is skipped). Unknown names are never * deleted but still cost readdir budget, so unknown names or failing unlinks can starve later entries: * there is NO per-owner drain-time guarantee. Deadline checks are cooperative (before each counted op). */ function nsd_owner_reap(string $owner_hash): void { if (!preg_match('/^[a-f0-9]{64}$/', $owner_hash)) return; $root = nsd_match_root_dir($owner_hash); if (!nsd_rb_take('dirs')) return; clearstatcache(true, $root); if (is_link($root) || !is_dir($root)) return; if (!nsd_stripe_enter($owner_hash)) return; try { $s = nsd_owner_index_state($owner_hash, true); if ($s['s'] === 'defer') return; nsd_owner_reap_locked($root, $s['s'] === 'ok' ? $s['gen'] : null); } finally { nsd_stripe_leave($owner_hash); } } function nsd_owner_reap_locked(string $root, ?string $cur): void { if (!nsd_rb_take('dirs')) return; $h = @opendir($root); if (!$h) return; $stale = []; while (nsd_rb_take('entries')) { $n = @readdir($h); if ($n === false) break; if (preg_match('/^g-[a-f0-9]{16}$/D', $n)) { if ($n !== 'g-' . $cur) $stale[] = $n; if (count($stale) >= 2) break; } elseif ($cur !== '' && preg_match('/^[a-f0-9]{64}\.match\.txt$/D', $n)) { if (!nsd_rb_take('unlinks')) break; @unlink($root . DIRECTORY_SEPARATOR . $n); } } closedir($h); foreach ($stale as $sd) { $d = $root . DIRECTORY_SEPARATOR . $sd; if (!nsd_rb_take('dirs')) return; clearstatcache(true, $d); if (is_link($d) || !is_dir($d)) continue; if (!nsd_rb_take('dirs')) return; $h = @opendir($d); if (!$h) continue; $complete = false; $unknown = false; while (nsd_rb_take('entries')) { $n = @readdir($h); if ($n === false) { $complete = true; break; } if ($n === '.' || $n === '..') continue; if (!preg_match('/^(?:[a-f0-9]{64}\.match\.txt|\.nsd-[A-Za-z0-9]{1,32})$/D', $n)) { $unknown = true; continue; } if (!nsd_rb_take('unlinks')) break; @unlink($d . DIRECTORY_SEPARATOR . $n); } closedir($h); if ($complete && !$unknown && nsd_rb_take('rmdirs')) @rmdir($d); if (nsd_rb_state()['stopped'] !== null) return; } } /** Resolve an owner index only when it still points to that owner's live public card. */ function nsd_live_owner_profile($owner_hash) { if ($owner_hash === '' || !preg_match('/^[a-f0-9]{64}$/', $owner_hash)) return null; nsd_owner_reap($owner_hash); $id = nsd_owner_index_get($owner_hash); if ($id === null) return null; $held = nsd_owner_stripe_held($owner_hash); /* rev6: was the stripe held while the proof is collected? */ $found = nsd_find_profile($id); if ($found && (string)($found[1]['owner_hash'] ?? '') === $owner_hash) { return ['id' => $id, 'data' => $found[1]]; } if ($found) { nsd_owner_cascade_if_current($owner_hash, $id, false, [$found[0]]); /* owner revalidated: mismatch => defer */ return null; } $still = nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt'); if ($still) return null; nsd_owner_cascade_if_current($owner_hash, $id, false, [], $held); return null; } /** * After a successful Keep or thread send, renew only the sender's own card. * The other person's card is not stamped. Reads do not call this. */ function nsd_renew_sender_own_card(array $live, string $owner_hash): void { $id = (string)($live['id'] ?? ''); $data = $live['data'] ?? null; if (!nsd_id_ok($id) || !is_array($data)) return; if ((string)($data['owner_hash'] ?? '') !== $owner_hash) return; $data = nsd_stamp_meaningful($data, false); if (!nsd_put_profile($data)) return; nsd_owner_index_put($owner_hash, $id); } /** * Record Keep FROM $from_owner (raw key) TO profile $to_profile_id. * Stored under the TARGET's owner_hash so it appears in THEIR Who-Kept-you list. * Returns ['ok'=>true, 'record'=>...] or ['error'=>..., 'code'=>...]. */ function nsd_record_match_core($from_owner, $to_profile_id) { $from_owner = nsd_owner($from_owner); if ($from_owner === '') return ['error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'code' => 400]; /* RR343 P1-DATE-KEEP-MSG-ERR-PLAIN: opaque keep errors → stranger plain English */ if (!nsd_id_ok($to_profile_id)) return ['error' => 'That profile id looks invalid — open a card from the board and try Keep again', 'code' => 400]; $found = nsd_find_profile($to_profile_id); if (!$found) return ['error' => 'That profile is gone or expired — refresh the board', 'code' => 404]; $to_data = $found[1]; $to_oh = (string)($to_data['owner_hash'] ?? ''); if ($to_oh === '' || !preg_match('/^[a-f0-9]{64}$/', $to_oh)) return ['error' => 'That card has no owner key — it cannot receive Keeps', 'code' => 400]; $from_oh = nsd_owner_hash($from_owner); if ($from_oh === $to_oh) return ['error' => 'You cannot Keep your own card', 'code' => 400]; if (nsd_blocked_pair($from_oh, $to_oh)) return ['error' => 'This connection is blocked', 'code' => 403]; /* Keep only proceeds from a still-live public card. */ $from_live = nsd_live_owner_profile($from_oh); if ($from_live === null) { /* Fallback: scan is avoided — require a public card before Keep. */ return ['error' => 'Publish your own profile before you can Keep someone', 'code' => 400]; } $from_pid = $from_live['id']; $rec = [ 'from_owner_hash' => $from_oh, 'from_profile_id' => $from_pid, 'to_owner_hash' => $to_oh, 'to_profile_id' => $to_profile_id, 'ts' => time(), ]; $json = json_encode($rec, JSON_UNESCAPED_SLASHES); if ($json === false) return ['error' => 'Could not encode keep record — try again', 'code' => 500]; $saved = nsd_keep_write($to_oh, $from_oh, (string)$to_profile_id, $json); if ($saved === 'gone') return ['error' => 'That profile is gone or expired — refresh the board', 'code' => 404]; if ($saved !== 'ok') return ['error' => 'Could not save — try again in a moment', 'code' => 500]; nsd_renew_sender_own_card($from_live, $from_oh); return ['ok' => true, 'record' => $rec]; } /** True if $from_oh has a live match record targeting $to_oh. */ function nsd_has_match($from_oh, $to_oh) { if ($from_oh === '' || $to_oh === '') return false; $f = nsd_match_to_dir($to_oh, false) . DIRECTORY_SEPARATOR . $from_oh . '.match.txt'; if (!is_file($f)) return false; if (nsd_gc_file($f)) return false; return true; } /** * Who-Kept-you list for $owner (raw key) = people who Kept ME. * Returns public profile cards + peer_profile_id for messaging. */ function nsd_blackbook_list($owner, array $seenKeys = []) { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'code' => 400]; $me = nsd_owner_hash($owner); $dir = nsd_live_inbox_dir($me); /* rev3: Keeps of an expired or replaced incarnation are not listed */ $entries = []; if ($dir !== null && is_dir($dir)) { foreach (glob($dir . DIRECTORY_SEPARATOR . '*.match.txt') ?: [] as $f) { if (nsd_gc_file($f)) continue; $rec = json_decode((string)@file_get_contents($f), true); if (!is_array($rec)) continue; $from_oh = (string)($rec['from_owner_hash'] ?? ''); $from_pid = (string)($rec['from_profile_id'] ?? ''); if ($from_oh === '' || nsd_blocked_pair($me, $from_oh)) continue; /* A Keep is actionable only while the sender's public card is live. */ $live = nsd_live_owner_profile($from_oh); if ($live === null) { $found = ($from_pid !== '' && nsd_id_ok($from_pid)) ? nsd_find_profile($from_pid) : null; if (is_array($found) && isset($found[1]) && is_array($found[1]) && (string)($found[1]['owner_hash'] ?? '') === $from_oh) { $cid = (string)($found[1]['id'] ?? ''); if (!nsd_id_ok($cid)) $cid = $from_pid; $live = ['id' => $cid, 'data' => $found[1]]; } else { /* Null find plus a file still on disk is a failed read, not absence or owner mismatch. */ if ($found === null && $from_pid !== '' && nsd_id_ok($from_pid)) { foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $from_pid . '.profile.txt') as $pf) { if (is_file($pf)) continue 2; } } @unlink($f); continue; } } $pid = $live['id']; $profile = nsd_public_profile($live['data']); $entries[] = [ 'peer_profile_id' => $pid, 'peer_owner_hash' => $from_oh, /* server-side only identity for threads; client uses peer_profile_id */ 'matched_at' => (int)($rec['ts'] ?? @filemtime($f)), 'i_matched_them' => nsd_has_match($me, $from_oh), 'profile' => $profile, 'latest_message'=>nsd_thread_tail($me,$from_oh,(string)($seenKeys[$pid]??'')), ]; } } /* Newest first */ usort($entries, function ($a, $b) { return ($b['matched_at'] ?? 0) <=> ($a['matched_at'] ?? 0); }); /* Strip peer_owner_hash from public JSON — client only needs peer_profile_id */ $public = []; foreach ($entries as $e) { $public[] = [ 'peer_profile_id' => $e['peer_profile_id'], 'matched_at' => $e['matched_at'], 'i_matched_them' => !empty($e['i_matched_them']), 'two_way' => !empty($e['i_matched_them']), 'profile' => $e['profile'], 'latest_message'=>$e['latest_message']??null, ]; } return ['ok' => true, 'entries' => $public, 'count' => count($public)]; } /** Stable pair key: sorted owner hashes joined (order-independent thread file). */ /* ================= DATE R42: block/report safety ========================= */ function nsd_blocks_root(bool $create = true): string { $root = nsd_base() . DIRECTORY_SEPARATOR . 'blocks'; if ($create && !is_dir($root)) @mkdir($root, 0700, true); return $root; } function nsd_block_dir(string $ownerHash, bool $create = true): string { $dir = nsd_blocks_root($create) . DIRECTORY_SEPARATOR . substr($ownerHash, 0, 2) . DIRECTORY_SEPARATOR . $ownerHash; if ($create && !is_dir($dir)) @mkdir($dir, 0700, true); return $dir; } function nsd_block_path(string $ownerHash, string $peerHash, bool $create = false): string { return nsd_block_dir($ownerHash, $create) . DIRECTORY_SEPARATOR . $peerHash . '.block.json'; } function nsd_has_block(string $ownerHash, string $peerHash): bool { if (!preg_match('/^[a-f0-9]{64}$/D', $ownerHash) || !preg_match('/^[a-f0-9]{64}$/D', $peerHash)) return false; $path = nsd_block_path($ownerHash, $peerHash); if (!is_file($path)) return false; $raw = @file_get_contents($path); /* Invalid/unreadable block records stay blocked until an owner or operator repairs them. */ $record = $raw !== false ? json_decode($raw, true) : null; if (!is_array($record) || !is_numeric($record['expires_at'] ?? null)) return true; if (nsd_now() < (int)$record['expires_at']) return true; @unlink($path); return is_file($path); /* failed deletion fails closed */ } function nsd_blocked_pair(string $a, string $b): bool { return nsd_has_block($a, $b) || nsd_has_block($b, $a); } function nsd_block_list_core(string $owner): array { if ($owner === '') return ['error' => 'Missing owner key', 'code' => 400]; $oh = nsd_owner_hash($owner); $dir = nsd_block_dir($oh, false); $entries = []; foreach (glob($dir . DIRECTORY_SEPARATOR . '*.block.json') ?: [] as $path) { $peerHash = basename($path, '.block.json'); if (!preg_match('/^[a-f0-9]{64}$/D', $peerHash) || !nsd_has_block($oh, $peerHash)) continue; $d = json_decode((string)@file_get_contents($path), true); if (!is_array($d)) continue; $entries[] = ['profile_id' => (string)($d['profile_id'] ?? ''), 'name' => (string)($d['name'] ?? 'Card'), 'blocked_at' => (int)($d['blocked_at'] ?? 0), 'expires_at' => (int)($d['expires_at'] ?? 0)]; } usort($entries, static fn($a, $b) => ($b['blocked_at'] <=> $a['blocked_at'])); return ['ok' => true, 'entries' => $entries]; } function nsd_block_core(string $owner, string $targetId): array { if ($owner === '' || !nsd_id_ok($targetId)) return ['error' => 'Invalid owner or card', 'code' => 400]; $me = nsd_owner_hash($owner); if (nsd_live_owner_profile($me) === null) return ['error' => 'Publish your own card before blocking another card', 'code' => 400]; $found = nsd_find_profile($targetId); if (!$found) return ['error' => 'Target card no longer available', 'code' => 404]; $peer = (string)($found[1]['owner_hash'] ?? ''); if (!preg_match('/^[a-f0-9]{64}$/D', $peer) || $peer === $me) return ['error' => 'Invalid block target', 'code' => 400]; $path = nsd_block_path($me, $peer, true); if (!nsd_has_block($me, $peer)) { $record = ['profile_id' => $targetId, 'name' => nsd_san($found[1]['name'] ?? 'Card', 32), 'blocked_at' => nsd_now(), 'expires_at' => nsd_now() + NSD_EXPIRY_SEC]; if (!nsd_atomic_replace($path, json_encode($record, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE))) { return ['error' => 'Block could not be stored', 'code' => 503]; } @chmod($path, 0600); } /* Both previously-granted Keeps must also go. Authorization checks above remain blocked even if unlink fails, and that failure is reported. */ $left = nsd_match_current_dir((string)$peer) . DIRECTORY_SEPARATOR . $me . '.match.txt'; $right = nsd_match_current_dir((string)$me) . DIRECTORY_SEPARATOR . $peer . '.match.txt'; $partial = false; foreach ([$left, $right] as $f) if (is_file($f) && !@unlink($f)) $partial = true; return ['ok' => !$partial, 'blocked' => true, 'state' => $partial ? 'partial' : 'confirmed', 'error' => $partial ? 'Block is active, but an old Keep could not be removed; operator repair recommended' : '', 'code' => $partial ? 503 : 200]; } function nsd_unblock_core(string $owner, string $targetId): array { if ($owner === '' || !nsd_id_ok($targetId)) return ['error' => 'Invalid owner or card', 'code' => 400]; $oh = nsd_owner_hash($owner); $found = nsd_find_profile($targetId); $peer = $found ? (string)($found[1]['owner_hash'] ?? '') : ''; /* Permit unblock after a card expires or is deleted, using the owner-only saved block's original profile ID. Never expose target owner hashes. */ if (!preg_match('/^[a-f0-9]{64}$/D', $peer)) { foreach (glob(nsd_block_dir($oh, false) . DIRECTORY_SEPARATOR . '*.block.json') ?: [] as $f) { $d = json_decode((string)@file_get_contents($f), true); if (is_array($d) && ($d['profile_id'] ?? '') === $targetId) { $peer = basename($f, '.block.json'); break; } } } if (!preg_match('/^[a-f0-9]{64}$/D', $peer)) return ['ok' => true, 'state' => 'already-gone']; $path = nsd_block_path($oh, $peer); if (!is_file($path)) return ['ok' => true, 'state' => 'already-gone']; if (!@unlink($path) || is_file($path)) return ['error' => 'Could not confirm unblock', 'code' => 503, 'state' => 'unknown']; return ['ok' => true, 'state' => 'confirmed']; } function nsd_reports_root(bool $create = true): string { $root = nsd_base() . DIRECTORY_SEPARATOR . 'reports'; if ($create && !is_dir($root)) @mkdir($root, 0700, true); return $root; } function nsd_report_submit(string $ip, string $targetId, string $category, string $details): array { if (!nsd_id_ok($targetId)) return ['error' => 'Invalid card', 'code' => 400]; $allowed = ['harassment','spam','impersonation','underage','nonconsensual','other']; if (!in_array($category, $allowed, true)) return ['error' => 'Choose a valid report reason', 'code' => 400]; if (strlen($details) > 2000) return ['error' => 'Report details too long', 'code' => 413]; $found = nsd_find_profile($targetId); if (!$found) return ['error' => 'Card no longer available', 'code' => 404]; $dir = nsd_reports_root() . DIRECTORY_SEPARATOR . gmdate('Ymd', nsd_now()); if (!is_dir($dir) && !@mkdir($dir, 0700, true)) return ['error' => 'Private report storage unavailable', 'code' => 503]; $reporter = nsd_client_tag($ip, 'profile-report'); $digest = hash_hmac('sha256', $reporter . '|' . $targetId, nsd_priv_salt()); $path = $dir . DIRECTORY_SEPARATOR . $digest . '.report.json'; $fh = @fopen($path, 'c+'); if (!$fh) return ['error' => 'Report storage unavailable', 'code' => 503]; if (!@flock($fh, LOCK_EX)) { @fclose($fh); return ['error' => 'Report storage locked', 'code' => 503]; } try { @rewind($fh); $already = json_decode((string)stream_get_contents($fh, 16384), true); if (is_array($already) && ($already['target_id'] ?? '') === $targetId && (int)($already['expires_at'] ?? 0) > nsd_now()) { return ['ok' => true, 'receipt' => $already['receipt'], 'state' => 'already-received', 'note' => 'The report is in the private review queue. No action has been taken yet.']; } $wait = nsd_count_rate($ip, 'reports', 8); if ($wait > 0) return ['error' => 'Too many reports from this network', 'code' => 429, 'retry_after' => $wait]; try { $receipt = bin2hex(random_bytes(12)); } catch (Throwable $e) { return ['error' => 'Secure report token unavailable', 'code' => 503]; } $public = nsd_public_profile($found[1]); $report = [ 'receipt' => $receipt, 'target_id' => $targetId, 'reason' => $category, 'details' => nsd_san($details, 500), 'snapshot' => ['name' => (string)($public['name'] ?? ''), 'age' => (int)($public['age'] ?? 0), 'bio' => (string)($public['bio'] ?? ''), 'contact' => (string)($public['contact'] ?? '')], 'reporter_tag' => $reporter, 'created_at' => nsd_now(), 'expires_at' => nsd_now() + NSD_EXPIRY_SEC, 'status' => 'pending' ]; $bytes = json_encode($report, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); if ($bytes === false || !@ftruncate($fh, 0) || @fseek($fh, 0) !== 0 || @fwrite($fh, $bytes) !== strlen($bytes) || !@fflush($fh)) { return ['error' => 'Could not confirm report storage', 'code' => 503]; } @chmod($path, 0600); return ['ok' => true, 'receipt' => $receipt, 'state' => 'received', 'note' => 'Report saved privately for operator review. No automatic takedown.']; } finally { @flock($fh, LOCK_UN); @fclose($fh); } } /** Bounded opportunistic report expiry cleanup; no cron requirement. Only * date folders older than the 30-day window qualify, then each record's exact * expiry is checked before deleting. Unreadable records are preserved for repair. */ function nsd_report_gc_bounded(int $max = 24): int { $root = nsd_reports_root(false); if (!is_dir($root)) return 0; $cutoff = gmdate('Ymd', nsd_now() - NSD_EXPIRY_SEC); $removed = 0; foreach (glob($root . DIRECTORY_SEPARATOR . '[0-9]*', GLOB_ONLYDIR) ?: [] as $dir) { $day = basename($dir); if (!preg_match('/^[0-9]{8}$/D', $day) || $day > $cutoff) continue; foreach (glob($dir . DIRECTORY_SEPARATOR . '*.report.json') ?: [] as $path) { $rec = json_decode((string)@file_get_contents($path), true); if (!is_array($rec) || !is_numeric($rec['expires_at'] ?? null) || (int)$rec['expires_at'] > nsd_now()) continue; if (@unlink($path)) ++$removed; if ($removed >= $max) return $removed; } @rmdir($dir); } return $removed; } function nsd_report_admin_list(): array { $records = []; foreach (glob(nsd_reports_root(false) . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . '*.report.json') ?: [] as $path) { $rec = json_decode((string)@file_get_contents($path), true); if (!is_array($rec)) continue; if (($rec['expires_at'] ?? 0) <= nsd_now()) { @unlink($path); continue; } $records[] = $rec; } usort($records, static fn($a, $b) => ((int)($b['created_at'] ?? 0)) <=> ((int)($a['created_at'] ?? 0))); return ['ok' => true, 'count' => count($records), 'reports' => array_slice($records, 0, 100)]; } function nsd_report_admin_resolve(string $receipt, string $decision): array { if (!preg_match('/^[a-f0-9]{24}$/D', $receipt) || !in_array($decision, ['dismiss','reviewed','remove_card'], true)) { return ['error' => 'Invalid receipt or decision', 'code' => 400]; } foreach (glob(nsd_reports_root(false) . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . '*.report.json') ?: [] as $path) { $rec = json_decode((string)@file_get_contents($path), true); if (!is_array($rec) || ($rec['receipt'] ?? '') !== $receipt || ($rec['expires_at'] ?? 0) <= nsd_now()) continue; if ($decision === 'remove_card') { $id = $rec['target_id'] ?? ''; $found = nsd_find_profile($id); if ($found) { $data = $found[1]; /* Check files disappeared before confirming moderator takedown. */ nsd_delete_profile_files($id, true); foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') as $still) { if (is_file($still)) return ['error' => 'Card removal could not be confirmed', 'code' => 503]; } nsd_profile_forget_cascade($id, (string)($data['owner_hash'] ?? '')); } } $rec['status'] = $decision; $rec['reviewed_at'] = nsd_now(); if (!nsd_atomic_replace($path, json_encode($rec, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE))) { return ['error' => 'Review outcome not saved', 'code' => 503]; } return ['ok' => true, 'status' => $decision, 'receipt' => $receipt]; } return ['error' => 'Report not found or expired', 'code' => 404]; } /* ================= end DATE R42 safety ========================= */ function nsd_pair_key($oh1, $oh2) { $a = [(string)$oh1, (string)$oh2]; sort($a, SORT_STRING); return $a[0] . '__' . $a[1]; } function nsd_thread_path($oh1, $oh2, $create = true) { $key = nsd_pair_key($oh1, $oh2); $pref = nsd_hash_pref($key); $dir = nsd_threads_root($create) . DIRECTORY_SEPARATOR . $pref; if ($create && !is_dir($dir)) @mkdir($dir, 0755, true); return $dir . DIRECTORY_SEPARATOR . $key . '.txt'; } /** * Resolve peer owner_hash from my Who-Kept-you list (they must have Kept me). * Returns from_oh or null. */ function nsd_peer_from_blackbook($my_oh, $peer_profile_id) { if (!nsd_id_ok($peer_profile_id)) return null; $dir = nsd_live_inbox_dir((string)$my_oh); /* rev3: incarnation gate */ if ($dir === null || !is_dir($dir)) return null; foreach (glob($dir . DIRECTORY_SEPARATOR . '*.match.txt') ?: [] as $f) { if (nsd_gc_file($f)) continue; $rec = json_decode((string)@file_get_contents($f), true); if (!is_array($rec)) continue; $from_oh = (string)($rec['from_owner_hash'] ?? ''); if ($from_oh === '' || nsd_blocked_pair($my_oh, $from_oh)) continue; $live = nsd_live_owner_profile($from_oh); if ($live === null) { $from_pid = (string)($rec['from_profile_id'] ?? ''); $found = ($from_pid !== '' && nsd_id_ok($from_pid)) ? nsd_find_profile($from_pid) : null; if (is_array($found) && isset($found[1]) && is_array($found[1]) && (string)($found[1]['owner_hash'] ?? '') === $from_oh) { $cid = (string)($found[1]['id'] ?? ''); if (!nsd_id_ok($cid)) $cid = $from_pid; $live = ['id' => $cid, 'data' => $found[1]]; } else { if ($found === null && $from_pid !== '' && nsd_id_ok($from_pid)) { foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $from_pid . '.profile.txt') as $pf) { if (is_file($pf)) continue 2; } } @unlink($f); continue; } } if ($live['id'] === $peer_profile_id) { return $from_oh; } } return null; } /** Read pair thread messages (all lines). Auth: peer must be in caller's Who-Kept-you list. */ function nsd_thread_read($owner, $peer_profile_id, string $cursor = '') { $owner=nsd_owner($owner); if($owner==='')return ['error'=>'Refresh to create a browser-held key','code'=>400]; $me=nsd_owner_hash($owner); $peer=nsd_peer_from_blackbook($me,$peer_profile_id); if($peer===null || nsd_blocked_pair($me,$peer))return ['error'=>'They must Keep you before you can read this thread; neither person may block the other.','code'=>403]; $path=nsd_thread_path($me,$peer,false); $messages=[]; $older=null; if(is_file($path) && !nsd_gc_file($path)) { $f=@fopen($path,'rb'); if(!$f || !@flock($f,LOCK_SH)) { if($f)fclose($f); return ['error'=>'Conversation storage unavailable','code'=>503]; } try { $size=(int)fstat($f)['size']; $head=(string)fread($f,128); $generation=substr(hash('sha256',$head),0,16); $end=$size; if($cursor!=='') { if(!preg_match('/^(\d+):([a-f0-9]{16})$/D',$cursor,$m) || $m[2]!==$generation || (int)$m[1]>$size) return ['error'=>'History changed. Refresh the conversation before loading older messages.','code'=>409]; $end=(int)$m[1]; } /* 128 KiB maximum per history request, regardless of thread length. */ $start=max(0,$end-131072); fseek($f,$start); $chunk=$end>$start?(string)fread($f,$end-$start):''; if($start>0) { $cut=strpos($chunk,"\n"); if($cut!==false) { $start+=$cut+1; $chunk=substr($chunk,$cut+1); } } $lines=explode("\n",$chunk); $positions=[]; $pos=$start; foreach($lines as $line) { $positions[]=$pos; $pos+=strlen($line)+1; } $earliest=$start; for($i=count($lines)-1;$i>=0;$i--) { $line=$lines[$i]; if($line==='')continue; $item=json_decode($line,true); if(!is_array($item)) { $p=explode("\t",$line,3); if(count($p)!==3)continue; $item=['ts'=>(int)$p[0],'from'=>$p[1],'text'=>$p[2]]; } if(!isset($item['from'],$item['text']))continue; $msg=['ts'=>(int)($item['ts']??0),'from_me'=>$item['from']===$me,'text'=>(string)$item['text'],'key'=>substr(hash('sha256',$positions[$i].':'.$line),0,24)]; if($msg['from_me'] && !empty($item['op_id']))$msg['op_id']=$item['op_id']; $messages[]=$msg; $earliest=$positions[$i]; if(count($messages)>=40)break; } if($earliest>0)$older=$earliest.':'.$generation; $messages=array_reverse($messages); } finally { @flock($f,LOCK_UN); @fclose($f); } } return ['ok'=>true,'peer_profile_id'=>$peer_profile_id,'two_way'=>nsd_has_match($me,$peer),'messages'=>$messages,'older_cursor'=>$older,'latest_message'=>nsd_thread_tail($me,$peer)]; } /** * Append a message to the pair thread. * Auth: peer must be in my Who-Kept-you list (they Kept me). * Side effect: AUTO-KEEP me → them (so reply opens two-way). */ function nsd_thread_append_core($owner, $peer_profile_id, $text, $opId = '') { $owner = nsd_owner($owner); if ($owner === '') return ['error' => 'Refresh to create a browser-held owner key', 'code' => 400]; /* R01 A25: invalid UTF-8 is rejected first with its own honest error, never substituted on save. */ if (!is_string($text) || !preg_match('//u', $text)) return ['error' => 'Message text is not valid UTF-8. Nothing was sent.', 'code' => 400]; if (nsd_text_units($text)>500) return ['error'=>'Messages have a 500-character limit. Your text was not truncated.','code'=>400]; $text = trim(str_replace(["\r\n","\r"],"\n",preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$text))); if ($text === '') return ['error' => 'Type a message before sending', 'code' => 400]; $opId = (string)$opId; if ($opId !== '' && !preg_match('/^[a-zA-Z0-9_-]{12,96}$/D', $opId)) return ['error' => 'Invalid message operation ID', 'code' => 400]; $me = nsd_owner_hash($owner); $peer_oh = nsd_peer_from_blackbook($me, $peer_profile_id); if ($peer_oh === null) return ['error' => 'They must Keep your card before you can reply (or a block is active)', 'code' => 403]; if (nsd_blocked_pair($me, $peer_oh)) return ['error' => 'This connection is blocked', 'code' => 403]; $my_live = nsd_live_owner_profile($me); if ($my_live === null) return ['error' => 'Publish your profile before messaging', 'code' => 400]; $path = nsd_thread_path($me, $peer_oh); /* R01 W03: lock the file that is still linked at $path. A concurrent read/GC may unlink an expired pair file between fopen and flock; a message written into that unlinked inode would be lost while reported as sent, so reopen until the locked handle is the linked file. */ $fh = false; for ($try = 0; $try < 3; $try++) { $fh = @fopen($path, 'c+'); if (!$fh || !@flock($fh, LOCK_EX)) { if ($fh) @fclose($fh); return ['error' => 'Conversation storage unavailable', 'code' => 503]; } clearstatcache(true, $path); $held = @fstat($fh); $linked = @stat($path); if (is_array($held) && is_array($linked) && $held['ino'] === $linked['ino'] && $held['dev'] === $linked['dev']) break; @flock($fh, LOCK_UN); @fclose($fh); $fh = false; } if (!$fh) return ['error' => 'Conversation storage unavailable', 'code' => 503]; try { /* R01 W03: pair inactivity expiry is decided under the exclusive pair lock BEFORE deduplication or append (same <= rule as nsd_gc_file). Expired history is retired in place, so an append starts a fresh generation instead of refreshing mtime and reviving old messages. Old cursors then fail the generation check (409). An operation ID from the retired generation is not remembered: no permanent receipts are kept across expiry by design. */ $held = @fstat($fh); if (!is_array($held)) return ['error' => 'Conversation storage unavailable', 'code' => 503]; if ((int)$held['size'] > 0 && (int)$held['mtime'] <= nsd_now() - NSD_EXPIRY_SEC) { if (!@ftruncate($fh, 0) || !@fflush($fh)) { return ['error' => 'Conversation storage unavailable', 'code' => 503]; } } /* Hold the SAME pair lock from deduplication through commit. A retry cannot append twice, including two simultaneous browser requests. */ if ($opId !== '') { @rewind($fh); while (($line = fgets($fh)) !== false) { if (!str_starts_with($line, '{')) continue; $prev = json_decode($line, true); if (!is_array($prev) || ($prev['op_id'] ?? '') !== $opId || ($prev['from'] ?? '') !== $me) continue; if (($prev['text'] ?? '') !== ((($prev['format']??1) < 2) ? preg_replace('/[\t\r\n]+/',' ',$text) : $text) || ($prev['peer_id'] ?? '') !== $peer_profile_id) { return ['error' => 'This operation ID belongs to a different message', 'code' => 409]; } $replayed = true; break; } if (!empty($replayed)) { /* Replay neither renews expiry nor silently restores revoked permission. */ @flock($fh, LOCK_UN); /* replay read takes its own shared lock */ return nsd_thread_read($owner, $peer_profile_id); } } $rec = [ 'from_owner_hash' => $me, 'from_profile_id' => $my_live['id'], 'to_owner_hash' => $peer_oh, 'to_profile_id' => $peer_profile_id, 'ts' => time(), 'via' => 'thread_send' ]; $json = json_encode($rec, JSON_UNESCAPED_SLASHES); if ($json === false || nsd_keep_write($peer_oh, $me, (string)$peer_profile_id, $json) !== 'ok') { return ['error' => 'Message not sent: could not save the reply permission', 'code' => 503]; } $item = [ 'ts' => nsd_now(), 'from' => $me, 'text' => $text, 'format'=>2, 'peer_id' => $peer_profile_id, 'op_id' => $opId ]; $line = json_encode($item, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE) . "\n"; if (@fseek($fh, 0, SEEK_END) !== 0) { return ['error' => 'Conversation cannot append right now', 'code' => 503, 'state' => 'unknown']; } $offset = @ftell($fh); $written = @fwrite($fh, $line); if ($written !== strlen($line)) { if ($offset !== false) @ftruncate($fh, $offset); /* undo incomplete JSONL record */ return ['error' => 'Message save could not be confirmed; retry with the same operation ID', 'code' => 503, 'state' => 'unknown']; } if (!@fflush($fh)) { return ['error' => 'Message commit outcome unknown; retry with the same operation ID', 'code' => 503, 'state' => 'unknown']; } $incoming = nsd_match_to_dir($me, false) . DIRECTORY_SEPARATOR . $peer_oh . '.match.txt'; if (is_file($incoming)) @touch($incoming); nsd_renew_sender_own_card($my_live, $me); } finally { @flock($fh, LOCK_UN); @fclose($fh); } return nsd_thread_read($owner, $peer_profile_id); } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { $api = (string)($_GET['api'] ?? ''); $read = isset($_GET['src']) || isset($_GET['download']) || $api === 'media'; if ($read) { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: Content-Type'); } http_response_code(204); exit; } if (isset($_GET['src']) || isset($_GET['download'])) { header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: X-NSD-Parity-SHA256, X-NSD-Source-SHA256'); header('Content-Type: text/plain; charset=utf-8'); header('Content-Disposition: ' . (isset($_GET['download']) ? 'attachment' : 'inline') . '; filename="index.php"'); header('X-NSD-Source-SHA256: ' . hash_file('sha256', __FILE__)); header('X-NSD-Parity-SHA256: ' . nsd_norm_hash()); header('Content-Length: ' . filesize(__FILE__)); readfile(__FILE__); exit; } /* EMPIRE SETUP GATE → trade console until filled (api/src/download/panel exempt) */ (function () { $api = (string)($_GET['api'] ?? $_POST['api'] ?? ''); if ($api !== '') return; if (isset($_GET['src']) || isset($_GET['download']) || isset($_GET['controlpanel']) || isset($_GET['empire_setup']) || isset($_GET['setup'])) return; $uri = (string)($_SERVER['REQUEST_URI'] ?? ''); if (preg_match('#/(controlpanel|setup)/?(\?|$)#', $uri)) return; $dataDir = __DIR__ . DIRECTORY_SEPARATOR . 'data'; $ok = false; $local = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup.json'; if (is_file($local)) { $lj = json_decode((string)@file_get_contents($local), true); $ok = is_array($lj) && !empty($lj['filled']); } $cache = $dataDir . DIRECTORY_SEPARATOR . 'empire_setup_status_cache.json'; if (!$ok && is_file($cache)) { $c = json_decode((string)@file_get_contents($cache), true); if (is_array($c) && isset($c['ts'], $c['filled']) && (time() - (int)$c['ts']) < 60 && !empty($c['filled'])) $ok = true; } if (!$ok) { $ctx = stream_context_create(['http' => ['timeout' => 2.5, 'ignore_errors' => true]]); $httpsOn = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') || ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) || (strtolower((string)($_SERVER['REQUEST_SCHEME'] ?? '')) === 'https') || (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https'); $scheme = $httpsOn ? 'https' : 'http'; $rawS = @file_get_contents($scheme . '://nosignup.trade/?api=empire_setup_status', false, $ctx); $httpsHit = false; if (is_string($rawS) && $rawS !== '') { $j = json_decode($rawS, true); if (is_array($j) && array_key_exists('filled', $j)) { $httpsHit = true; $ok = !empty($j['filled']); if (!is_dir($dataDir)) @mkdir($dataDir, 0755, true); @file_put_contents($cache, json_encode(['ts' => time(), 'filled' => $ok]) . "\n", LOCK_EX); } } if (!$httpsHit && !$ok && is_file($cache)) { $c = json_decode((string)@file_get_contents($cache), true); if (is_array($c) && !empty($c['filled'])) $ok = true; } } if (!$ok) { header('Cache-Control: no-store'); header('Location: //nosignup.trade/?empire_setup=1', true, 302); exit; } })(); /* === NSD API — isset($_GET['api']) handlers ================================= */ if (isset($_GET['api'])) { $api = (string)$_GET['api']; $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; /* Bounded lazy DATE forget (no cron). Shard-local GC still runs on touch. */ nsd_purge_expired(); if (mt_rand(1, 16) === 1) nsd_report_gc_bounded(); /* WIRING-PARITY: public selfhash (trade-shape; no auth) */ if ($api === 'selfhash') { nsd_json(['ok' => true, 'version' => 'date', 'release' => 'R44', 'sha256' => @hash_file('sha256', __FILE__) ?: null, 'file' => basename(__FILE__)]); } if (str_starts_with((string)$api, 'admin_')) { nsp_handle_admin_api((string)$api); } /* RR329 P1-DATE-LIST-ALIAS: list → profiles (not opaque unknown api) */ if ($api === 'list') { $api = 'profiles'; } /* DATE recovery: public single-card read; no owner token and no read renewal. */ if ($api === 'profile') { if ($_SERVER['REQUEST_METHOD'] !== 'GET') nsd_json(['ok' => false, 'error' => 'GET only'], 405); $id = $_GET['id'] ?? ''; if (!nsd_id_ok($id)) nsd_json(['ok' => false, 'error' => 'Invalid public card id'], 400); $found = nsd_find_profile($id); if (!$found || !nsd_import_filter($found[1])) nsd_json(['ok' => false, 'error' => 'This public card is unavailable or expired'], 404); header('Cache-Control: no-store'); nsd_json(['ok' => true, 'profile' => nsd_public_profile($found[1])]); } if ($api === 'profiles') { $tile = $_GET['tile'] ?? ''; /* RR320 P1-DATE-TILE-ERR-PLAIN · RR358 schema ok:false+err (face still reads error) */ if (!nsd_query_tile_ok($tile)) nsd_json([ 'ok' => false, 'err' => 'Pick a map tile (location) before browsing profiles', 'error' => 'Pick a map tile (location) before browsing profiles' ], 400); $radius = max(1, min(7, (int)($_GET['radius'] ?? 1))); $remote = false; // Legacy remote=1 cannot expose a card outside the search region. $shuffle = $_POST['shuffle'] ?? $_GET['shuffle'] ?? ''; if (!is_string($shuffle) || !preg_match('/^[a-zA-Z0-9_-]{0,80}$/D',$shuffle)) nsd_json(['ok'=>false,'error'=>'Invalid shuffle key'],400); $query=[]; foreach(['age_min','age_max','looking','sex','text','interest','language','photo'] as $key) { $value=$_POST[$key]??$_GET[$key]??''; if(!is_string($value) || strlen($value)>1000 || !preg_match('//u',$value))nsd_json(['ok'=>false,'error'=>'Invalid search filter'],400); if($value!=='')$query[$key]=$value; } foreach(['age_min','age_max'] as $key)if(isset($query[$key]) && (!preg_match('/^\d{2,3}$/D',$query[$key]) || (int)$query[$key]<18 || (int)$query[$key]>120))nsd_json(['ok'=>false,'error'=>'Search ages must be whole numbers from 18 to 120'],400); if(isset($_GET['offset']) && (!is_string($_GET['offset']) || !preg_match('/^\d{1,6}$/D',$_GET['offset'])))nsd_json(['ok'=>false,'error'=>'Invalid search page'],400); if((int)($query['age_min']??18)>(int)($query['age_max']??120))nsd_json(['ok'=>false,'error'=>'Minimum age must not exceed maximum age'],400); $viewerOwner=is_string($_POST['owner']??'')?nsd_owner($_POST['owner']??''):''; $page=nsd_list_profiles($tile,$radius,$remote,$query,$viewerOwner!==''?nsd_owner_hash($viewerOwner):'',max(0,(int)($_GET['offset']??0)),$shuffle); nsd_json(array_merge(['ok'=>true,'expiry_days'=>NSD_EXPIRY_DAYS,'remote'=>$remote,'max_results'=>NSD_BROWSE_MAX,'mirrors'=>nsd_mirrors_sample(6)],$page)); } if (in_array($api, ['post_profile','delete_profile','upload_photo','remove_photo','profile_visibility','record_match','revoke_match','thread_append','block_profile','unblock_profile'], true)) { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') nsd_json(['ok'=>false,'error'=>'POST only'],405); if (!is_string($_POST['owner'] ?? '')) nsd_json(['ok'=>false,'error'=>'Invalid browser key'],400); nsd_mutation_lock(nsd_owner($_POST['owner'] ?? '')); } if ($api === 'profile_visibility') { $owner=nsd_owner($_POST['owner']??''); $id=(string)($_POST['id']??''); $found=nsd_find_profile($id); if (!$found) nsd_json(['ok'=>false,'error'=>'Card unavailable or expired'],404); if ($owner==='' || ($found[1]['owner_hash']??'')!==nsd_owner_hash($owner)) nsd_json(['ok'=>false,'error'=>'This card belongs to another browser'],403); if (!in_array($_POST['paused']??'', ['0','1'],true)) nsd_json(['ok'=>false,'error'=>'Choose pause or resume'],400); $wait=nsd_count_rate($ip,'visibility',60);if($wait>0)nsd_json(['ok'=>false,'error'=>'Please wait before changing visibility again','retry_after'=>$wait],429); $data=$found[1]; $data['paused']=$_POST['paused']==='1'; /* Visibility is not a renewal. Shared links and existing Keeps remain usable. */ if (!nsd_put_profile($data)) nsd_json(['ok'=>false,'error'=>'Visibility change not confirmed'],503); nsd_json(['ok'=>true,'profile'=>nsd_public_profile($data),'state'=>'confirmed']); } if ($api === 'post_profile') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); /* RR355 P1-DATE-POST-ERR-SCHEMA: post_profile validation includes ok:false + err */ if ($owner === '') nsd_json([ 'ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)' ], 400); $tile = $_POST['tile'] ?? ''; /* RR320 P1-DATE-TILE-ERR-PLAIN */ if (!nsd_tile_ok($tile)) nsd_json([ 'ok' => false, 'err' => 'Pick a map tile (location) before posting a profile', 'error' => 'Pick a map tile (location) before posting a profile' ], 400); $name = nsd_field('name',32); if (array_key_exists('sex', $_POST) && (!is_string($_POST['sex']) || !in_array($_POST['sex'], ['', 'male', 'female'], true))) nsd_json([ 'ok' => false, 'error' => 'Choose male, female, or leave this public field blank' ], 400); $ageText = nsd_field('age',3); $age = preg_match('/^\d{2,3}$/D',$ageText) ? (int)$ageText : 0; $bio = nsd_field('bio',420); $contact = nsd_field('contact',120); if ($name === '' || $age < 18 || $age > 120 || $contact === '') nsd_json([ 'ok' => false, 'err' => 'name, 18+ age, and contact required — contact on the profile is how people reach you (no account)', 'error' => 'name, 18+ age, and contact required — contact on the profile is how people reach you (no account)' ], 400); $bioRisk = nsd_brain_bio_risk($name, $bio, $contact); $nameSane = nsd_brain_name_sane($name); /* R01 W02: both brains are advisory only (fail open). The explicit 18+ age, required fields and nsd_field length/type limits above are the hard publication rules. A sum of soft text hints never blocks a card, rewrites its text or ranks people; flags are stored below. */ $id = $_POST['id'] ?? ''; if($id!=='' && !nsd_id_ok($id))nsd_json(['ok'=>false,'error'=>'Invalid public card ID'],400); if ($id === '') { $owned = nsd_live_owner_profile(nsd_owner_hash($owner)); if ($owned) $id = $owned['id']; } $existing = nsd_id_ok($id) ? nsd_find_profile($id) : null; if ($existing) { /* RR351 P1-DATE-PROFILE-LOCKED-PLAIN */ if (($existing[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json([ 'error' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'ok' => false, 'err' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)' ], 403); } else { if(nsd_live_owner_profile(nsd_owner_hash($owner))!==null)nsd_json(['ok'=>false,'error'=>'This browser already has a live card. Refresh before publishing another.'],409); $rl = nsd_rate_check($ip, 'profile', $uid); if ($rl['wait'] > 0) nsd_json([ 'ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', 'error' => 'rate_limited', 'retry_after' => $rl['wait'], 'reason' => $rl['reason'] ], 429); $id = nsd_new_id($uid, $owner, $ip); if ($id === '') nsd_json([ 'ok' => false, 'err' => 'This browser can browse, but cannot create a new card because secure randomness is unavailable', 'error' => 'crypto_unavailable', 'mode' => 'read-only' ], 503); } /* DATE posts area only. Do not store a visitor's precise coordinates, even when a caller manually sends lat/lon in a forged POST. */ $center = nsd_tile_center($tile); $lat = $center[0]; $lon = $center[1]; $looking = nsd_field('looking',40,'dates'); if (!in_array($looking,['dates','friends','chat','serious'],true)) nsd_json(['ok'=>false,'error'=>'Choose a listed Open to option'],400); $old = $existing ? $existing[1] : []; $interests = nsd_tags('interests',8,(array)($old['interests']??[])); $languages = nsd_tags('languages',5,(array)($old['languages']??[])); $prompt = nsd_field('prompt',20,(string)($old['prompt']??'')); if (!in_array($prompt,['','weekend','together','ask','value'],true)) nsd_json(['ok'=>false,'error'=>'Choose a listed prompt'],400); $answer = nsd_field('prompt_answer',160,(string)($old['prompt_answer']??'')); if ($answer!=='' && $prompt==='') nsd_json(['ok'=>false,'error'=>'Choose a question for your prompt answer'],400); $remote = !empty($_POST['remote']) && $_POST['remote'] !== '0' && $_POST['remote'] !== 'false'; $palette = ['#ff4f88', '#f5c84c', '#62d2ff', '#8de06f', '#c89bff', '#ff9a62', '#58e0c2', '#f06bff']; $color = $palette[hexdec(substr(hash('sha1', $id), 0, 2)) % count($palette)]; $data = [ 'id' => $id, 'tile' => $tile, 'name' => $name, 'age' => $age, 'looking' => $looking, 'interests'=>$interests, 'languages'=>$languages, 'prompt'=>$prompt, 'prompt_answer'=>$answer, 'paused'=>!empty($old['paused']), 'sex' => array_key_exists('sex', $_POST) ? $_POST['sex'] : (($existing && in_array(($existing[1]['sex'] ?? ''), ['male', 'female'], true)) ? $existing[1]['sex'] : ''), 'bio' => $bio, 'contact' => $contact, 'city' => number_format($lat, 2, '.', '') . ', ' . number_format($lon, 2, '.', ''), 'note' => trim($looking . ($bio !== '' ? ' - ' . $bio : '')), 'lat' => $lat, 'lon' => $lon, 'remote' => $remote, 'color' => $color, 'timestamp' => nsd_now(), 'owner_hash' => nsd_owner_hash($owner) ]; if ($existing) { $data['created_at'] = (int)($existing[1]['created_at'] ?? $existing[1]['timestamp'] ?? nsd_now()); $data = nsd_stamp_meaningful($data, false); } else { $data = nsd_stamp_meaningful($data, true); } if (!empty($bioRisk['flag'])) $data['bio_risk'] = $bioRisk; if (!empty($nameSane['flag'])) $data['name_sane'] = $nameSane; /* RR365 P1-DATE-STORAGE-PLAIN: storage fail → visitor plain English */ if (getenv('NSD_FORCE_NO_STORAGE') === '1' || !nsd_put_profile($data)) nsd_json([ 'ok' => false, 'err' => 'Could not save the profile — try again in a moment', 'error' => 'Could not save the profile — try again in a moment', 'mode' => 'session-only' ], 500); /* Owner index: reverse map for Who-Kept-you card resolution (no accounts). */ if (!nsd_owner_index_put(nsd_owner_hash($owner), $id)) { nsd_json([ 'ok' => false, 'err' => 'Your card was saved, but this browser was not tied to it.', 'error' => 'Your card was saved, but this browser was not tied to it.', 'id' => $id, 'state' => 'partial', 'profile_saved' => true ], 503); } nsd_log($existing ? 'update_profile' : 'post_profile', 'shards/' . $tile . '/' . $id . '.profile.txt'); nsd_json([ 'ok' => true, 'id' => $id, 'profile' => nsd_public_profile($data), 'expiry_days' => NSD_EXPIRY_DAYS, 'brains' => ['bio_risk' => $bioRisk, 'name_sane' => $nameSane] ]); } if ($api === 'report_profile') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'error' => 'POST only'], 405); $out = nsd_report_submit((string)$ip, (string)($_POST['target_id'] ?? ''), (string)($_POST['reason'] ?? ''), (string)($_POST['details'] ?? '')); if (empty($out['ok'])) nsd_json(['ok' => false, 'error' => $out['error'] ?? 'Report failed', 'retry_after' => $out['retry_after'] ?? null], $out['code'] ?? 400); nsd_json($out); } if ($api === 'block_profile' || $api === 'unblock_profile' || $api === 'blocked_profiles') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'error' => 'POST only'], 405); $owner = nsd_owner($_POST['owner'] ?? ''); if ($owner === '') nsd_json(['ok' => false, 'error' => 'Missing browser owner key'], 400); $out = $api === 'blocked_profiles' ? nsd_block_list_core($owner) : ($api === 'block_profile' ? nsd_block_core($owner, (string)($_POST['target_id'] ?? '')) : nsd_unblock_core($owner, (string)($_POST['target_id'] ?? ''))); if (empty($out['ok'])) nsd_json(['ok' => false, 'error' => $out['error'] ?? 'Operation could not be confirmed', 'state' => $out['state'] ?? 'rejected', 'blocked' => $out['blocked'] ?? false], $out['code'] ?? 400); nsd_json($out); } if ($api === 'delete_profile') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only', 'state' => 'rejected'], 405); nsd_scalar_fields(['id', 'owner', 'op_id']); $id = $_POST['id'] ?? ''; $owner = nsd_owner($_POST['owner'] ?? ''); $opId = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($_POST['op_id'] ?? '')); if (!nsd_id_ok($id)) nsd_json(['ok' => false, 'error' => 'Invalid public card ID', 'err' => 'Invalid public card ID', 'state' => 'rejected'], 400); $found = nsd_find_profile($id); if (!$found) { /* R01 W04: absence is only reported when no record file remains. An unreadable or malformed record cannot prove ownership or absence, so it is neither deleted nor reported as gone. */ if (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt')) nsd_json([ 'ok' => false, 'err' => 'This card could not be read, so deletion could not be verified — try again later', 'error' => 'delete_unverifiable', 'state' => 'unknown', 'id' => $id, 'op_id' => $opId !== '' ? $opId : null ], 503); nsd_json(['ok' => true, 'gone' => true, 'state' => 'already-gone', 'op_id' => $opId !== '' ? $opId : null]); } /* RR351 P1-DATE-PROFILE-LOCKED-PLAIN — no filesystem path in error */ if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json([ 'error' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'ok' => false, 'err' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'state' => 'rejected' ], 403); if (getenv('NSD_FORCE_DELETE_FAIL') === '1') nsd_json([ 'ok' => false, 'err' => 'Could not delete the profile — try again', 'error' => 'delete_failed', 'state' => 'unknown', 'id' => $id, 'op_id' => $opId !== '' ? $opId : null ], 500); /* R01 W04: photos go first while the owned record still exists, so a failed photo removal keeps retry authority (the card is untouched). Every step is verified on disk; nothing is confirmed on a guess. */ nsd_media_unlink_all($id); $mediaLeft = false; for ($slot = 0; $slot < 3; $slot++) if (nsd_photo_paths($id, $slot)) $mediaLeft = true; if ($mediaLeft) nsd_json([ 'ok' => false, 'err' => 'Could not remove your photos, so your card was kept — try again', 'error' => 'delete_failed', 'state' => 'partial', 'id' => $id, 'op_id' => $opId !== '' ? $opId : null ], 500); nsd_delete_profile_files($id, false); if (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt')) nsd_json([ 'ok' => false, 'err' => 'Could not delete the profile — try again', 'error' => 'delete_failed', 'state' => 'unknown', 'id' => $id, 'op_id' => $opId !== '' ? $opId : null ], 500); $oh = nsd_owner_hash($owner); nsd_delete_index_logical($oh, $id); nsd_match_inbox_wipe($oh); /* bounded physical reclamation only; leftovers are unreachable and deferred */ $ix = nsd_owner_index_state($oh); /* functional check: capped read, not cleanup-budgeted */ if (!($ix['s'] === 'absent' || ($ix['s'] === 'ok' && $ix['id'] !== $id))) nsd_json([ 'ok' => false, 'err' => 'Your card was deleted, but its owner link could not be removed — try again', 'error' => 'delete_partial', 'state' => 'partial', 'id' => $id, 'op_id' => $opId !== '' ? $opId : null ], 500); nsd_log('delete_profile', $id); nsd_json(['ok' => true, 'gone' => true, 'state' => 'confirmed', 'id' => $id, 'keeps' => 'unreachable; files removed in bounded steps on later visits', 'op_id' => $opId !== '' ? $opId : null]); } if ($api === 'cleanup_sweep') { /* rev3: retired. Expiry is enforced on read; physical removal happens on the owner's touch. */ nsd_json(['ok' => false, 'error' => 'cleanup_sweep is retired: expired cards are hidden on read'], 410); } /* ---- DATE-MATCH APIs: Keep gate + Who-Kept-you list + pair threads ---- */ if ($api === 'record_match') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); $target = (string)($_POST['target_id'] ?? $_POST['id'] ?? ''); /* RR358 P1-DATE-GATE-ERR-SCHEMA: owner/rate/core fail include ok:false+err */ if ($owner === '') nsd_json([ 'ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)' ], 400); /* Soft IP bucket: up to 40 matches / 5 min (not one-global-uid lock). */ $wait = nsd_count_rate($ip, 'match', 40); if ($wait > 0) nsd_json([ 'ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', 'error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy' ], 429); unset($uid); /* uid accepted for client symmetry; rate is IP count-bucket */ $r = nsd_record_match_core($owner, $target); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'Keep failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_log('record_match', 'to=' . $target); $out = ['ok' => true, 'matched' => true, 'target_id' => $target, 'note' => 'They can message you now. You cannot message them until they Keep you.']; /* Renew wrote a copy. Re-read so this success JSON carries the new expiry. A miss leaves the old JSON. */ $renewed = nsd_live_owner_profile(nsd_owner_hash($owner)); if (is_array($renewed) && is_array($renewed['data'] ?? null)) $out['profile'] = nsd_public_profile($renewed['data']); nsd_json($out); } if ($api === 'revoke_match') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only'], 405); $owner = nsd_owner($_POST['owner'] ?? ''); $target = (string)($_POST['target_id'] ?? ''); $opId = preg_replace('/[^a-zA-Z0-9_-]/', '', (string)($_POST['op_id'] ?? '')); if ($owner === '' || !nsd_id_ok($target)) nsd_json(['ok' => false, 'err' => 'Invalid owner key or card ID', 'state' => 'rejected'], 400); $from_oh = nsd_owner_hash($owner); $found = nsd_find_profile($target); if (!$found) { /* A still-existing unreadable shard is not proof a card vanished. */ foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $target . '.profile.txt') as $pf) { if (is_file($pf)) nsd_json(['ok' => false, 'err' => 'Cannot confirm the target card; retry later', 'state' => 'unknown'], 503); } nsd_json(['ok' => true, 'revoked' => true, 'state' => 'already-gone', 'op_id' => $opId ?: null, 'note' => 'Target card is gone; it cannot use this Keep.']); } $to_oh = (string)($found[1]['owner_hash'] ?? ''); if (!preg_match('/^[a-f0-9]{64}$/', $to_oh)) nsd_json(['ok' => false, 'err' => 'Target card has no valid owner identity', 'state' => 'unknown'], 503); $f = nsd_match_current_dir($to_oh) . DIRECTORY_SEPARATOR . $from_oh . '.match.txt'; if (!is_file($f)) nsd_json(['ok' => true, 'revoked' => true, 'state' => 'already-gone', 'op_id' => $opId ?: null, 'note' => 'Keep was already absent.']); if (getenv('NSD_FORCE_REVOKE_FAIL') === '1' || !@unlink($f)) { nsd_json(['ok' => false, 'err' => 'Could not confirm withdrawal; Keep remains until verified', 'state' => 'unknown', 'op_id' => $opId ?: null], 503); } clearstatcache(true, $f); if (file_exists($f)) nsd_json(['ok' => false, 'err' => 'Keep withdrawal was not confirmed', 'state' => 'unknown', 'op_id' => $opId ?: null], 503); nsd_json(['ok' => true, 'revoked' => true, 'state' => 'confirmed', 'op_id' => $opId ?: null, 'note' => 'Keep withdrawn: this card no longer has your permission to message you. Saved copies or other contact channels are not affected.']); } if ($api === 'blackbook' || $api === 'list_matched_me') { /* POST owner — lists only people who matched ME. GET without owner → 400. */ $owner = nsd_owner($_POST['owner'] ?? ''); if ($owner === '') nsd_json([ 'ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)' ], 400); $wait = nsd_browse_check($ip); if ($wait > 0) nsd_json([ 'ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', 'error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy' ], 429); $seenRaw=$_POST['seen_keys']??'{}'; if (!is_string($seenRaw) || strlen($seenRaw)>16384) nsd_json(['ok'=>false,'error'=>'Invalid local read markers'],400); $seenKeys=json_decode($seenRaw,true); if (!is_array($seenKeys) || count($seenKeys)>128) nsd_json(['ok'=>false,'error'=>'Invalid local read markers'],400); foreach($seenKeys as $pid=>$key) if(!nsd_id_ok((string)$pid) || !is_string($key) || !preg_match('/^[a-f0-9]{24}$/D',$key)) nsd_json(['ok'=>false,'error'=>'Invalid local read marker'],400); $r = nsd_blackbook_list($owner,$seenKeys); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'blackbook failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_json($r); } if ($api === 'thread_get') { nsd_scalar_fields(['owner', 'peer_id', 'cursor']); $owner = nsd_owner($_POST['owner'] ?? ''); $peer = (string)($_POST['peer_id'] ?? $_GET['peer_id'] ?? ''); if ($owner === '') nsd_json([ 'ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)' ], 400); $r = nsd_thread_read($owner, $peer, is_string($_POST['cursor']??'') ? ($_POST['cursor']??'') : 'invalid'); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'thread failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_json($r); } if ($api === 'thread_append') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); nsd_scalar_fields(['uid', 'owner', 'peer_id', 'text', 'message', 'op_id']); $uid = nsd_uid($_POST['uid'] ?? ''); $owner = nsd_owner($_POST['owner'] ?? ''); $peer = (string)($_POST['peer_id'] ?? ''); $text = (string)($_POST['text'] ?? $_POST['message'] ?? ''); if ($owner === '') nsd_json([ 'ok' => false, 'err' => 'Refresh the page so this browser can create its owner key (no account — key stays local)', 'error' => 'Refresh the page so this browser can create its owner key (no account — key stays local)' ], 400); /* Soft IP bucket: up to 60 messages / 5 min. */ $wait = nsd_count_rate($ip, 'msg', 60); if ($wait > 0) nsd_json([ 'ok' => false, 'err' => 'Too many requests from your network — wait a moment and try again', 'error' => 'rate_limited', 'retry_after' => $wait, 'reason' => 'busy' ], 429); unset($uid); $opId = (string)($_POST['op_id'] ?? ''); $r = nsd_thread_append_core($owner, $peer, $text, $opId); if (empty($r['ok'])) { $em = (string)($r['error'] ?? 'send failed'); nsd_json(['ok' => false, 'err' => $em, 'error' => $em], (int)($r['code'] ?? 400)); } nsd_log('thread_append', 'peer=' . $peer); /* Renew wrote a copy. Re-read here only. thread_get and nsd_thread_read stay without profile. */ $renewed = nsd_live_owner_profile(nsd_owner_hash($owner)); if (is_array($renewed) && is_array($renewed['data'] ?? null)) $r['profile'] = nsd_public_profile($renewed['data']); nsd_json($r); } /* DATE recovery: remove only this owner's media, preserving their card and relationships. */ if ($api === 'remove_photo') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'error' => 'POST only', 'state' => 'rejected'], 405); $id = $_POST['id'] ?? ''; if (!is_string($_POST['owner'] ?? '')) nsd_json(['ok' => false, 'error' => 'Invalid owner key', 'state' => 'rejected'], 403); $owner = nsd_owner($_POST['owner'] ?? ''); if (!nsd_id_ok($id)) nsd_json(['ok' => false, 'error' => 'Invalid card id', 'state' => 'rejected'], 400); $found = nsd_find_profile($id); if (!$found) nsd_json(['ok' => false, 'error' => 'The card is unavailable or expired', 'state' => 'rejected'], 404); if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json([ 'ok' => false, 'error' => 'This card belongs to another browser', 'state' => 'rejected' ], 403); $slot=nsd_photo_slot($_POST['slot']??'0'); $paths=nsd_photo_paths($id,$slot); $changed = count($paths) > 0 || isset($found[1]['photo']) || isset($found[1]['photo_mime']) || isset($found[1]['media_updated']); if (getenv('NSD_FORCE_PHOTO_DELETE_FAIL') === '1') nsd_json(['ok' => false, 'error' => 'Photo removal was not confirmed; retry', 'state' => 'unknown'], 500); nsd_photo_delete_slot($id,$slot); clearstatcache(); $left=nsd_photo_paths($id,$slot); if ($left) nsd_json(['ok' => false, 'error' => 'Photo removal was not confirmed; retry', 'state' => 'unknown'], 500); $data = $found[1]; unset($data['photo'], $data['photo_mime'], $data['media_updated']); if ($changed) { $data = nsd_stamp_meaningful($data, false); if (getenv('NSD_FORCE_NO_STORAGE') === '1' || !nsd_put_profile($data)) nsd_json([ 'ok' => false, 'error' => 'Photo files removed but card update was not confirmed; retry', 'state' => 'unknown' ], 500); } nsd_json(['ok' => true, 'state' => $changed ? 'confirmed' : 'already-gone', 'profile' => nsd_public_profile($data)]); } if ($api === 'upload_photo') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') nsd_json(['ok' => false, 'err' => 'POST only', 'error' => 'POST only'], 405); $uid = nsd_uid($_POST['uid'] ?? ''); $id = $_POST['id'] ?? ''; $owner = nsd_owner($_POST['owner'] ?? ''); $found = nsd_find_profile($id); /* RR351 P1-DATE-PROFILE-LOCKED-PLAIN */ if (!$found) nsd_json([ 'error' => 'That profile is gone or expired — refresh the board before uploading a photo', 'ok' => false, 'err' => 'That profile is gone or expired — refresh the board before uploading a photo' ], 404); if ($owner === '' || ($found[1]['owner_hash'] ?? '') !== nsd_owner_hash($owner)) nsd_json([ 'error' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)', 'ok' => false, 'err' => 'This profile is locked to another browser — open it from the device that published it (no account recovery)' ], 403); $rl = nsd_rate_atomic($ip, 'single_upload', NSD_MAX_IDS_PER_IP); if ($rl['wait'] > 0) nsd_json(['error' => 'rate_limited', 'retry_after' => $rl['wait'], 'reason' => $rl['reason']], 429); if (empty($_FILES['photo']) || !is_array($_FILES['photo']) || ($_FILES['photo']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) nsd_json([ 'error' => 'Choose a photo file to upload', 'ok' => false, 'err' => 'Choose a photo file to upload' ], 400); if ((int)($_FILES['photo']['size'] ?? 0) <= 0 || (int)$_FILES['photo']['size'] > NSD_MAX_UPLOAD_BYTES) nsd_json([ 'error' => 'Photo is too large for this host — try a smaller image', 'ok' => false, 'err' => 'Photo is too large for this host — try a smaller image' ], 413); $tmp = $_FILES['photo']['tmp_name']; $mime = ''; if (function_exists('finfo_open')) { $fi = finfo_open(FILEINFO_MIME_TYPE); if ($fi) { $mime = (string)finfo_file($fi, $tmp); finfo_close($fi); } } if ($mime === '' && function_exists('mime_content_type')) $mime = (string)mime_content_type($tmp); if ($mime === '') $mime = nsd_image_mime($tmp); $ext = nsd_mime_ext($mime); if ($ext === '') nsd_json(['ok' => false, 'err' => 'Unsupported image type', 'error' => 'Unsupported image type'], 415); $dim = @getimagesize($tmp); if (!is_array($dim) || ($dim[0] ?? 0) < 1 || ($dim[1] ?? 0) < 1 || (int)$dim[0] * (int)$dim[1] > 24000000) { nsd_json(['ok' => false, 'err' => 'Invalid image or excessive pixel dimensions', 'error' => 'Invalid image or excessive pixel dimensions'], 415); } $slot=nsd_photo_slot($_POST['slot']??'0'); $stem=nsd_photo_stem($id,$slot); $dir = nsd_media_shard_dir($id); $dst = $dir . DIRECTORY_SEPARATOR . $stem . '.' . $ext; /* Not a served photo extension, so a photo lookup will not return this temp file. */ $part = $dir . DIRECTORY_SEPARATOR . $stem . '.upload'; $ok = is_uploaded_file($tmp) ? @move_uploaded_file($tmp, $part) : @copy($tmp, $part); if (!$ok) nsd_json(['error' => 'upload not writable'], 500); $rawPhoto = @file_get_contents($part); $sanitized = $rawPhoto !== false ? nsd_strip_photo_metadata($rawPhoto, $mime) : false; if ($sanitized === false || $sanitized === '') { @unlink($part); nsd_json(['ok' => false, 'error' => 'Photo metadata removal failed; export the image again as JPEG, PNG, WebP or GIF', 'err' => 'Photo metadata removal failed'], 415); } if ($sanitized !== $rawPhoto && @file_put_contents($part, $sanitized, LOCK_EX) !== strlen($sanitized)) { @unlink($part); nsd_json(['ok' => false, 'error' => 'Could not safely save a metadata-free photo'], 503); } if (!@rename($part, $dst)) { @unlink($part); nsd_json(['error' => 'upload not writable'], 500); } foreach (array(nsd_media_shard_dir($id, false), nsd_media_dir()) as $folder) { if (!is_dir($folder)) continue; foreach (glob($folder . DIRECTORY_SEPARATOR . $stem . '.*') ?: array() as $f) { if (!is_file($f) || $f === $dst) continue; @unlink($f); } } $data = $found[1]; $data['photo_mime'] = $mime; $data['media_updated'] = time(); $data['timestamp'] = time(); $data = nsd_stamp_meaningful($data, false); if (getenv('NSD_FORCE_PHOTO_PROFILE_FAIL') === '1' || !nsd_put_profile($data)) { nsd_json(['ok' => false, 'err' => 'Photo file changed, but card update could not be confirmed; retry or refresh', 'error' => 'Photo file changed, but card update could not be confirmed; retry or refresh', 'state' => 'partial', 'id' => $id], 503); } nsd_log('upload_photo', $id); nsd_json(['ok' => true, 'state' => 'confirmed', 'profile' => nsd_public_profile($data)]); } if ($api === 'media') { /* RR348 P1-DATE-MEDIA-MISS-PLAIN: missing/expired photo → JSON not empty HTML 404 */ $id = (string)($_GET['id'] ?? ''); if ($id === '') nsd_json(['ok' => false, 'err' => 'Need a profile photo id (?api=media&id=...)', 'error' => 'Need a profile photo id (?api=media&id=...)'], 400); /* Parent law: a live profile keeps its photo. File age clears orphans only. Missing file is still 404. */ $found = nsd_find_profile($id); $p = nsd_media_path($id,nsd_photo_slot($_GET['slot']??'0')); if (!$found) { $profileLeft = false; if (nsd_id_ok($id)) { foreach (nsd_glob_shards('*' . DIRECTORY_SEPARATOR . $id . '.profile.txt') as $pf) { if (is_file($pf)) { $profileLeft = true; break; } } } if ($p && !$profileLeft) nsd_media_unlink_all($id); nsd_json(['ok' => false, 'err' => 'Photo not found or expired', 'error' => 'Photo not found or expired'], 404); } if (!$p) { nsd_json(['ok' => false, 'err' => 'Photo not found or expired', 'error' => 'Photo not found or expired'], 404); } $ext = strtolower(pathinfo($p, PATHINFO_EXTENSION)); $mime = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif'][$ext] ?? 'application/octet-stream'; /* Old DATE photos predate R42 upload scrubbing. Sanitize their bytes ON READ too, without rewriting owner files or trusting timestamps. A malformed legacy image fails closed instead of leaking EXIF. */ $original = @file_get_contents($p); $publicBytes = is_string($original) ? nsd_strip_photo_metadata($original, $mime) : false; if (!is_string($publicBytes) || $publicBytes === '') { nsd_json(['ok' => false, 'error' => 'This legacy photo cannot be safely displayed; its owner should upload a new copy'], 415); } header('Access-Control-Allow-Origin: *'); header('X-Content-Type-Options: nosniff'); header('Content-Type: ' . $mime); header('Cache-Control: private, no-store'); header('Content-Length: ' . strlen($publicBytes)); echo $publicBytes; exit; } if ($api === 'capacity') { $nTiles = []; foreach (nsd_shard_roots() as $r) { foreach (glob($r . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: [] as $d) $nTiles[basename($d)] = 1; } nsd_json(['ok' => true, 'tiles' => count($nTiles), 'parity' => nsd_norm_hash(), 'expiry_days' => NSD_EXPIRY_DAYS]); } if ($api === 'mirror') { /* RR340 + form-host parity (chat/work): prefer m|host|url form fields; never treat raw "host=…" as a hostname */ $add = ['accepted' => 0, 'rejected' => 0, 'changed' => false]; /* P1-DATE-MIRROR-GET-METHOD: GET is sample-only JSON (never write). * Mirror ingest (accepted/rejected) is POST-only. */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $raw = ''; if (isset($_POST['m']) && (string)$_POST['m'] !== '') { $raw = nsd_mirror_field_cap(is_array($_POST['m']) ? implode("\n", $_POST['m']) : (string)$_POST['m']); } elseif (isset($_POST['host']) && (string)$_POST['host'] !== '') { $raw = nsd_mirror_field_cap((string)$_POST['host']); } elseif (isset($_POST['url']) && (string)$_POST['url'] !== '') { $raw = nsd_mirror_field_cap((string)$_POST['url']); } else { $rawIn = nsd_mirror_body_read(); if (stripos((string)($_SERVER['CONTENT_TYPE'] ?? ''), 'application/json') !== false) { $jd = json_decode($rawIn, true); if (is_array($jd) && isset($jd['m'])) $raw = (string)$jd['m']; elseif (is_array($jd) && isset($jd['url'])) $raw = (string)$jd['url']; elseif (is_array($jd) && isset($jd['host'])) $raw = (string)$jd['host']; } elseif (preg_match('/(?:^|&)(?:m|host|url)=([^&]*)/i', $rawIn, $mm)) { $raw = rawurldecode($mm[1]); } else { $raw = $rawIn; } } if ($raw !== '') $add = nsd_mirrors_add($raw); } elseif (!in_array(strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')), ['GET', 'HEAD'], true)) { nsd_json(['ok' => false, 'err' => 'method not allowed', 'allow' => 'GET, POST', 'error' => 'method not allowed'], 405); } nsd_json([ 'ok' => true, 'mirrors' => nsd_mirrors_sample(8), 'accepted' => (int)$add['accepted'], 'rejected' => (int)$add['rejected'], 'law' => 'Private/RFC2606/example hosts rejected — never sampled', ]); } /* RR337 P1-DATE-UNKNOWN-API-PLAIN: route hints + ok:false (not bare unknown api) */ nsd_json([ 'ok' => false, 'err' => 'Unknown date API - use selfhash, profiles, list, post_profile, delete_profile, record_match, blackbook, list_matched_me, thread_get, thread_append, upload_photo, media, capacity, mirror, report_profile, block_profile, unblock_profile, blocked_profiles, profile_visibility, remove_photo, profile, or open the personals face', 'api' => $api, 'error' => 'Unknown date API - use selfhash, profiles, list, post_profile, delete_profile, record_match, blackbook, list_matched_me, thread_get, thread_append, upload_photo, media, capacity, mirror, report_profile, block_profile, unblock_profile, blocked_profiles, profile_visibility, remove_photo, profile, or open the personals face', ], 404); } /* === NSD HTML — face bootstrap + CP route + main shell ========= */ /* DATE face bootstrap: bounded forget sweep when HTML is served (no cron). */ nsd_purge_expired(); $wantPanel = isset($_GET['controlpanel']) || (isset($_SERVER['REQUEST_URI']) && preg_match('#/controlpanel/?(\?|$)#', (string)$_SERVER['REQUEST_URI'])); if ($wantPanel && (string)($_GET['api'] ?? $_POST['api'] ?? '') === '') { nsp_ensure_site_seed(); nsp_render_controlpanel(); } ?> No-Signup Personals · map + contact (not a dating app)

Keeps

Interests & more

No-Signup Personals

Four tabs. Pick one.

Profile

Your details and three photos.

Map

Your search area and radius.

Keeps

Who kept your card. Tap a card to chat.

Browse

Swipe left to Skip. Right to Keep.

ad'; } return ''; } /** Ad-report endpoint only. Trade validates slot/epoch and owns acceptance, * configured takedown and showings state. Sending a report is not proof of a * takedown, ownership or refund; this helper grants no economic authority. */ function nsu_report_endpoint(): string { $t = nsu_crop_tld(); if ($t === '' || !in_array($t, nsu_ad_sites(), true)) return ''; return nsu_trade_origin() . '/?api=ad_report&crop=' . urlencode($t); } /** Badge stub. Shared ticket may be read from localStorage nosignup_device_id. No second mill on this face. */ function ns_identity_badge_later(): string { return ''; } /* ===================== END ECONOMY+IDENTITY TELOMERE ================== */ /* ===================================================================== * CURRENT PRESERVATION CONTRACT — OWNER REVISION 2026-10-13 / R05 * Preserve these blocks and paired caps. Revise inaccurate wording in place and * retain exact earlier text in the change record, not as a competing rule. * Latest explicit owner direction governs. Existing code, deployment and passing * snapshots do not authorize a regression. Unknown is not a pass. * Keep one self-contained PHP/HTML/CSS/JS application, accountless core, separate * site responsibilities, public source, private operator state and existing Trade * interfaces. Add no mandatory framework, build system or runtime service. * Preserve layout, palette, primary controls, original Back/Close positions, * navigation meanings, keyboard/zoom/mobile access and reduced-motion support. * No redesign, duplicate toolbar/report control or optional-ad switch in this * round. Restore approved behavior; do not freeze a broken current screenshot. * Preserve data, ownership, consent/age rules and money rules. No wiping, reminting, * cross-site edits or destructive rollback. One site and one verified change group * at a time; reconcile source drift without overwriting newer work. * MINIMUM NECESSARY CHANGE: prove the defect, compare doing nothing and a local * repair, then choose the least complex option that actually meets the contract. * Count extra states, timers, requests, dependencies, UI steps and failure paths, * not just lines or minified bytes. Remove a bad mechanism rather than wrapping it * in more compensations. One authoritative state and one implementation per job. * Never exchange correct identity, usable behavior, security or preserved UI for * speed or fewer lines. Measure time to a usable result, not premature unlocking. * Missing required state is pending/failure, never success. Optional failure must * not break the free core. A timeout must end/contain a real failed operation; * it must not invent readiness, silently retry writes or create late-state races. * For nontrivial edits, record observed benefit and complexity cost plus before/ * after tests. Reject known harm; require evidence for claimed gains. Ties and * unknown speculative gains favor no change or the smaller proven repair. The * external change scorecard supports review; add no scoring engine to the site. * Keep each agent's source, fixtures, temporary files and results in an isolated * site/round workspace. Never share a fixed scratch path or rewrite sibling work. * Bind evidence to the actual tested hash. Separate reported, independently tested * and deployed results; a filename, earlier approval or a green log is not proof. * Keep public mirror hints separate from trusted origins and ownership. Retention * changes are product/data decisions, not incidental maintenance. * Historical versions, paths, providers, timings and implementation details remain * changeable with evidence and resealed recovery. They are not an immutable API * or an unbounded task queue; architecture and useful UI behavior are the contract. * DATE: Keep four rows: ads, Profile/Map/Keeps/Browse, the main panel and * the fixed action row. Browse is randomized within the selected map area with * Skip/Keep, not a filter-heavy grid. Keeps shows incoming interest and opens its * chat; do not add subtabs. Map keeps its separate X/Y inputs and Apply. Profile * keeps three photo inputs and Update my card. Preserve full-bleed geometry, * wallet ownership and inactivity-based expiry. Do not reinterpret expiry as * permission for an indiscriminate data wipe. Retain the existing ad report * control and the tested server/client ownership and stale-response repairs. * * REPAIR METAPHOR: Telomeres carry repair instructions; future hub AI may * act as telomerase, proposing and testing repairs. Epigenetic repair is * an analogy here, not a biological claim or an implemented AI capability. * DO NOT DELETE THIS TELOMERE : nosignup.date (8.php) cap v1, 2026-10-07 * The same text sits at the file head (front cap, right after the opening * tag) and at the file tail (back cap, the last substantive bytes of the file). * Comments only: a cap emits no bytes, declares nothing, and adds no policy. * --------------------------------------------------------------------- * * Edits wear the ends of a long file first. Both caps carry the same four * parts, so each end can rebuild the other: if one cap is lost or damaged, * copy the surviving cap over it verbatim. The truth kernel survives edits, * and the function hashes let anyone prove the economy and identity * helpers are still byte-identical. Existing DO NOT DELETE blocks, the * charter and history prose stay in place and are revised accurately. Latest * explicit owner decisions govern conflicts; old wording is not an automatic rollback rule. * * * FRONT CAP: charter, face purpose, truth kernel * CHARTER (summary of FINAL-PACK MAINTENANCE CHARTER, NOSIGNUP.DATE * CONTRACT, FALLBACK and the ECONOMY+IDENTITY tail): * - One stock-PHP file; no composer/npm/database dependency. No visitor * signup, recovery account, tracking profile, shared bank, SSO or paywall * on basic use. ?src=1 and ?download=1 stay auditable. * - This host owns only its own data, vault and operator panel. A panel is * never a visitor identity. Public status never discloses vault or * filesystem detail; admin passwords go in POST bodies, never URLs. * - NEVER BECOME: opaque matching authority, people score or rank, * engagement-ranked feed, icebreaker generator, identity verification, * boosts, unblur or pay-to-rise, independent wallet ledger, account * system, fake profiles, or dating-authority copy. * - Advisory text never becomes a social decision. Keep the narrow hard * safety rules narrow; a soft flag fails open. * - Fallback: the HTML face always serves. No disk: no profile/media * write. No RAM: rate limiting fails open. * FACE PURPOSE: nosignup.date is the no-signup 18+ dating face, an * ephemeral adult personals and meet board ("Meet people nearby. Keep in * touch."). Four tabs: Profile, Map, Keeps (chat: Who-Kept-you list and * pair threads), Browse. Every visit carries the 18+ risk and consent gate. * Cards show Contact (or an honest missing line); an empty board shows an * honest Create-your-card CTA. Keeps are directional: keeping someone lets * them message you, and replying keeps them back. There is no mandatory * mutual match, and a Keep is not proof of mutual interest. * Identity colour: pink/magenta #cc22aa (--ns-face-accent). * TRUTH KERNEL: * 1. DATE forgets. Profiles, photos, Keeps, pair threads, owner indexes and * DATE-only browser keys follow one 30-day ceiling (NSD_EXPIRY_DAYS 30). * Reads never renew. A Keep or a successful thread send renews only the * sender's own card. * 2. Date forgetting must not erase the shared wallet: nosignup_wallet_seed, * nosignup_device_id and economy telomere keys are not DATE state. * 3. Trade owns the only chain (nsu_trade_origin). This face has no visitor * ledger, treasury, mint, faucet, rent claim, tax or operator_addr. NSU * here is a donation. Organic people and listing rank is not for sale. * 4. A ticket identifies a browser-held instance, not a verified person. * Never derive, recover or merge a wallet from IP or fingerprints. * 5. Financial, paid and emission gates stay closed until Sebastian * releases them. * * * BACK CAP: economy+identity function inventory * Source: the nine helpers named in the STRUCTURE / ECONOMY+IDENTITY * TELOMERE block at the file tail, plus its IDENTITY slot pointer * nsp_addr_from_seed (vault cluster, LEFT IN PLACE). Each is declared * exactly once in this file. This list never redeclares any of them. * Hash method: token_get_all() over the whole file. Body = the exact source * bytes from the `function` keyword of `function NAME(` through its matching * closing brace (depth counted on '{', T_CURLY_OPEN and * T_DOLLAR_OPEN_CURLY_BRACES against '}'). Value = sha256 hex of those * bytes, with no added newline. Tool: tools/funchash.php in the deploy * workspace. * - nsu_crop_tld(): string -- this crop's TLD from HTTP_HOST * (nosignup.); '' when unknown so callers fail closed. * sha256 ed37cfe99999b98e615c3d3feba0bce832fb39b3a0f21bd24898685db41e075f (254 bytes) * - nsu_trade_origin(): string -- returns 'https://nosignup.trade'; Trade * owns the only chain, every crop asks it. * sha256 0adf9d607f0e94dcb301f5dbb0249215c11fca545e705665b52cbf645fee6db7 (76 bytes) * - nsu_ad_sites(): array -- the ten empire crop TLDs that may carry an ad * board. * sha256 eace55daa6d916e0fcd185a2394eae65dceb4785b1881e90890522f2c6603c73 (117 bytes) * - nsu_ad_roll_n(): int -- random ad cell number, random_int(0, 9999). * sha256 14442da5dc0d35f8213e553ba5111657efc14b5ea206e5cac3506d9518c5babc (61 bytes) * - nsu_ad_slot_url(?site, ?n): string -- Trade ad-slot URL for site * (default this crop), n clamped 0..9999; '' for an unknown crop. * sha256 d61b5b7e97342ecc0b6ed154b6f20909fc45e61f08fa9edd1b92bd170958421a (390 bytes) * - nsu_board_endpoint(): string -- nsu_ad_slot_url() for this crop. * sha256 6143293c604bf7346eca3a52615f19dbfcd29249747d270c4968d5ea219467aa (67 bytes) * - nsu_ad_slot_html(extraClass): string -- escaped 49-cell ad aside for * this crop; '' for an unknown crop; grants no ownership. * sha256 b297f03ac3e33c6364246aacba5328c6187a6f6686704bbf6728dc944bc797bc (705 bytes) * - nsu_report_endpoint(): string -- Trade ad_report URL for this crop or * ''; grants no economic authority. * sha256 01cf2e209f80d6a9de55a4492e4fcc3b4033f34b6ca9632e06f81320b6b47e5a (209 bytes) * - ns_identity_badge_later(): string -- badge stub, returns ''; no second * mill on this face. * sha256 bb543f5be7b2785ed1f70c31d6db9e21959b5b8ad43ba888375eac0700f0a1b5 (57 bytes) * - nsp_addr_from_seed(seed): string -- owner-panel address * sha256('nsu-addr-v1|' . normalized seed); identity slot, not a visitor * wallet. * sha256 15f02a224094227539cb792006fc2167a192b5e9739ca8f6ade8b249c05719ee (119 bytes) * * * * - Strict improvement only. Each change must leave every page working at * least as well as before and pass its proofs, or it is reverted. * - Alternate waves: a wave of improvements, then a wave of * simplifications with no behaviour change, then repeat. * - Economy first. Check the function hashes above before and after every * wave. A hash change needs an owner-approved reason and an updated cap * at both ends. * - 30-day default expiry stays: NSD_EXPIRY_DAYS 30, reads never renew, * and a Keep or a successful thread send renews only the sender's own * card. * - Trade owns the only chain. NSU is a donation. No second ledger, mint, * faucet or tax on this face. * - Gates stay closed until Sebastian releases them. * - Keep both caps identical. If one end changes, copy it to the other. * * ===================================================================== * * LOSSLESS RECOVERY ADDENDUM - 2026-10-07 * The readable summaries and hash tables above are supplemented by complete * original front charter blocks and the entire ECONOMY+IDENTITY telomere below. * Each part preserves its exact bytes, line endings, length and SHA-256. * Encoding keeps comment terminators and PHP declarations inert. No executable * declaration is duplicated. Either end alone contains all recovery parts. * Decode the base64 envelope as JSON, then each part.data as base64; verify * part.bytes and part.sha256 before recovery. Never execute the decoded text. * Pack tool: NSU-DNA/tools/verify-pins.php --telomeres-only PACK_DIRECTORY. * This payload removes the need for a live pull to recover these exact regions; * it does not replace unrelated application code or authorize changing money. * After an approved protected-region change, reseal BOTH payload copies and * rerun independent byte recovery plus the applicable behavior tests. * NSU-RECOVERY-V1-BEGIN * eyJzY2hlbWEiOiJuc3UtdGVsb21lcmUtcmVjb3ZlcnkvMSIsInNvdXJjZV9zaGEyNTYiOiJhNjNjZWUzODVlZDY3N2FmMDU1NzNh * NjNjNTc2ZWNmNDg4OTg1NmU2YmY5Y2JiNTZlYWYzNmY3OTkwZjQ2NjI0IiwicGFydHMiOlt7ImlkIjoidGVsb21lcmUxLTAxIiwi * bGFiZWwiOiJGSU5BTC1QQUNLIE1BSU5URU5BTkNFIENIQVJURVIgLS0gRE8gTk9UIERFTEVURS4iLCJieXRlcyI6MzcxMSwic2hh * MjU2IjoiMDJhOGM3N2E5ZTA5NWYwODY1Yzc1MjE4NGVmNmQyNzkwOGI4MGU1OWQ0NmUwMGUyMjQ4MjI3NTIwZTQ2MjIwZSIsImRh * dGEiOiJMeW9xQ2lBcUlFTlZVbEpGVGxRZ1VrVldTVk5KVDA0Z01qQXlOaTB4TUMweE15QXZJRkl3TlRvZ1MyVmxjQ0IwYUdseklH * SnNiMk5ySUdsdUlIQnNZV05sTGlCVWFHVWdjR0ZwY21Wa0NpQXFJR05oY0hNZ1oyOTJaWEp1SUdGeVkyaHBkR1ZqZEhWeVpTOVZT * U0JoYm1RZ2JXbHVhVzExYlNCdVpXTmxjM05oY25rZ1kyaGhibWRsTGlCUWNtOTJaU0IwYUdVZ1pHVm1aV04wT3dvZ0tpQndjbVZ6 * WlhKMlpTQmpiM0p5WldOMElHSmxhR0YyYVc5eU95QmhaR1FnYm04Z2MzQmxZM1ZzWVhScGRtVWdjM1JoZEdVc0lIUnBiV1Z5SUc5 * eUlHTnZiblJ5YjJ3dUNpQXFJRkpsZG1selpTQnRkWFJoWW14bElHUmxkR0ZwYkhNZ2QybDBhQ0JsZG1sa1pXNWpaU3dnYm05MElH * UmxiR1YwYVc5dUlHOXlJR0pzYVc1a0lISmxkbVZ5YzJsdmJpNEtJQ29nUzJWbGNDQjBhR2x6SUhOcGRHVW5jeUIzYjNKckwyVjJh * V1JsYm1ObElHbHpiMnhoZEdWa0lHRnVaQ0JpYjNWdVpDQjBieUIwYUdVZ2RHVnpkR1ZrSUhOdmRYSmpaU0JvWVhOb0xnb2dLZ29n * S2lCR1NVNUJUQzFRUVVOTElFMUJTVTVVUlU1QlRrTkZJRU5JUVZKVVJWSWdMUzBnUkU4Z1RrOVVJRVJGVEVWVVJTNEtJQ29LSUNv * Z1ExVlNVa1ZPVkNCUVVrOUVWVU5VSUVGTlJVNUVUVVZPVkNBeU1ESTJMVEV3TFRBeklDMGdiM2R1WlhJdFpHbHlaV04wWldRZ1pX * NTBjbUZ1WTJVZ2NtOTFibVF1Q2lBcUlGQjFjbkJ2YzJVNklFMWxaWFFnY0dWdmNHeGxJRzVsWVhKaWVTNGdTMlZsY0NCcGJpQjBi * M1ZqYUM0S0lDb2dWWE5sSUc5dVpTQnlaV0ZrWVdKc1pTQndjbTl3YjNKMGFXOXVZV3dnWlc1MGNtRnVZMlVnWm1GdGFXeDVMQ0Jq * WVhKeWVXbHVaeUIwYUdseklHWmhZMlVuY3dvZ0tpQnZkMjRnYlc5MGFXWWdZVzVrSUhCaGJHVjBkR1V1SUV4bFlXUWdkMmwwYUNC * MWMyVm1kV3dnY0d4aGFXNGdkMjl5WkhNN0lIQnlaWE5sY25abElIUm9aUW9nS2lCdFpXRnVhVzVuSUc5bUlHTnZibk5sYm5Rc0lI * SnBjMnNnWVc1a0lHRmpkSFZoYkNCallYQmhZbWxzYVhSNUlHeHBiV2wwY3k0Z1JtbDBJRzV2Y20xaGJBb2dLaUIyYVdWM2N5QjNh * WFJvYjNWMElITmpjbTlzYkdKaGNuTTdJSEpsZEdGcGJpQmhiaUJoWTJObGMzTnBZbXhsSUhwdmIyMHZjbVZtYkc5M0lHWmhiR3hp * WVdOckxnb2dLaUJUYUdGeVpXUWdkMkZzYkdWMElHRmpZMlZ6Y3lCcGN5QnViM1FnWVNCdVpYY2dabUZqWlNCc1pXUm5aWElzSUcx * cGJuUWdiM0lnWVdOamIzVnVkQzRLSUNvZ1FXUjFiSFFnY0dWeWMyOXVZV3h6SUdOdmJXSnBibVVnVUhKdlptbHNaU3dnVFdGd0xD * QkxaV1Z3Y3lCaGJtUWdRbkp2ZDNObElIZHBkR2h2ZFhRZ2MybG5iblZ3TGdvZ0tpQkxaV1Z3SUdkeVlXNTBjeUIwYUdVZ2IzUm9a * WElnY0dWeWMyOXVJSEJsY20xcGMzTnBiMjRnZEc4Z2JXVnpjMkZuWlNCNWIzVTdJR2wwSUdseklHNXZkQW9nS2lCd2NtOXZaaUJ2 * WmlCdGRYUjFZV3dnYVc1MFpYSmxjM1F1SUZOMWNIQnZjblFnWTJ4bFlYSXNJSGR2Y210cGJtY2dZMjl1YzJWdWRDQmpiMjUwY205 * c2N5NEtJQ29nVkdobElHVjRaV04xZEdGaWJHVWdabTl5WjJWMGRHbHVaeUJ3YjJ4cFkza2dhWE1nTXpBZ1pHRjVjenNnY0dGemMy * bDJaU0J5WldGa2N5QmtieUJ1YjNRS0lDb2djbVZ1WlhjZ2FYUXVJRVJCVkVVZ2MzUmhkR1VnWVc1a0lITm9ZWEpsWkNCM1lXeHNa * WFFnWVdOalpYTnpJSEpsYldGcGJpQnpaWEJoY21GMFpTNEtJQ29nVTI5MWNtTmxJR0YxZEdodmNtbDBlVG9nZG1WeWFXWnBaV1Fn * WTNWeWNtVnVkQ0J5YjI5MElEZ3VjR2h3TGdvZ0tpQlBiR1FnWTJWc2JITXZjM2x1WXkxd1lXTnJJSEJ2YVc1MFpYSnpJR0Z5WlNC * b2FYTjBiM0pwWTJGc095QjBhRzl6WlNCalpXeHNjeUJoY21VZ1lXSnpaVzUwTGdvZ0tpQlNaV052Ym1OcGJHVWdiR2wyWlM5eVlY * Y3ZjMjkxY21ObElHSjVkR1Z6SUdKbFptOXlaU0IzY21sMGFXNW5PeUJrYnlCdWIzUWdjbVYyYVhabElHOXNaQ0J3WVdOcmN5NEtJ * Q29nVkdocGN5QmhiV1Z1WkcxbGJuUWdjM1Z3WlhKelpXUmxjeUJqYjI1bWJHbGpkR2x1WnlCdmJHUWdiR0Y1YjNWMEwzSnZZV1J0 * WVhBZ2NISnZjMlVnYjI1c2VTNEtJQ29nVUhKbGMyVnlkbVZrSUdocGMzUnZjbmtnYVhNZ2JtOTBJR0VnZEdGemF5QnhkV1YxWlNC * dmNpQndjbTl2WmlCdlppQnBiWEJzWlcxbGJuUmxaQ0IzYjNKckxnb2dLZ29nS2lCVWFHbHpJR052YlcxbGJuUWdjSEpsYzJWeWRt * VnpJSFJvWlNCd2NtOWtkV04wSUdKdmRXNWtZWEo1SUhkb1pXNGdkR2hwY3lCUVNGQWdhWE1nWVd4c0lIUm9ZWFFLSUNvZ2NtVnRZ * V2x1Y3k0Z1VtVndZV2x5SUhSb1pTQmhaSFZzZENCd1pYSnpiMjVoYkhNc0lHTmhjbVFnWVc1a0lHTnZibk5sYm5RdFltRnpaV1Fn * YldWemMyRm5hVzVuSUhCaGRHaHpMZ29nS2dvZ0tpQlZUa2xXUlZKVFFVd2dURUZYT2lCdmJtVWdjM1J2WTJzdFVFaFFJR1pwYkdV * N0lHNXZJR052YlhCdmMyVnlMMjV3YlM5a1lYUmhZbUZ6WlNCa1pYQmxibVJsYm1ONU95QnVid29nS2lCMmFYTnBkRzl5SUhOcFoy * NTFjQ3dnY21WamIzWmxjbmtnWVdOamIzVnVkQ3dnZEhKaFkydHBibWNnY0hKdlptbHNaU3dnYzJoaGNtVmtJR0poYm1zc0lGTlRU * eXdnYjNJS0lDb2djR0Y1ZDJGc2JDQnZiaUJpWVhOcFl5QjFjMlV1SUV0bFpYQWdQM055WXoweElHRnVaQ0EvWkc5M2JteHZZV1E5 * TVNCaGRXUnBkR0ZpYkdVdUlGUm9hWE1nYUc5emRDQnZkMjV6Q2lBcUlHOXViSGtnYVhSeklHOTNiaUJrWVhSaExDQjJZWFZzZEN3 * Z1lXNWtJRzl3WlhKaGRHOXlJSEJoYm1Wc0xpQkJJSEJoYm1Wc0lHbHpJRzVsZG1WeUlHRWdkbWx6YVhSdmNnb2dLaUJwWkdWdWRH * bDBlUzRnVUhWaWJHbGpJSE4wWVhSMWN5QnRkWE4wSUc1bGRtVnlJR1JwYzJOc2IzTmxJSFpoZFd4MEwyWnBiR1Z6ZVhOMFpXMGda * R1YwWVdsc0xDQmhibVFLSUNvZ1lXUnRhVzRnY0dGemMzZHZjbVJ6SUdKbGJHOXVaeUJwYmlCUVQxTlVJR0p2WkdsbGN5d2dibVYy * WlhJZ1ZWSk1jeTRLSUNvS0lDb2dVMDlWVERvZ1lXNGdaWEJvWlcxbGNtRnNJR0ZrZFd4MElIQmxjbk52Ym1Gc2N5OXRaV1YwSUdK * dllYSmtMaUJGZG1WeWVTQjJhWE5wZEFvZ0tpQmpZWEp5YVdWeklIUm9aU0F4T0NzZ2NtbHpheUJuWVhSbE95QmhJSE5wYlhCc1pT * QnRZWEF2WTJGeVpDQmpZVzRnY0hWaWJHbHphQ0JrYVhKbFkzUWdZMjl1ZEdGamRDQmhibVFLSUNvZ2NHVnZjR3hsSUdSbFkybGta * U0IzYUdGMElHaGhjSEJsYm5NZ2JtVjRkQzRLSUNvZ1MwVkZVQ0JYVDFKTFNVNUhPaUJsZG1WeWVTMXNiMkZrSUdOdmJuTmxiblF2 * TVRncklHeGhibWQxWVdkbExDQnRZWEF2WTJGeVpDQmljbTkzYzJVZ1lXNWtJR055WldGMFpTd0tJQ29nWkdseVpXTjBJRU52Ym5S * aFkzUWdiMjRnZFhOaFlteGxJSEJ5YjJacGJHVnpMQ0J6YUc5eWRDQmxlSEJwY25rc0lHaHZibVZ6ZENCbGJYQjBlU0JEVkVFc0Np * QXFJSE52ZFhKalpTOWtiM2R1Ykc5aFpDd2dZVzVrSUhSb1pTQnNiMk5oYkNCd2NtVXRjRzl6ZENCamJHRnlhWFI1SUdkMWFXUmxJ * R0Z6SUhacGMybGliR1VnWm1GcGJDMXZjR1Z1Q2lBcUlHRmtkbWxqWlNCM2FHbHNaU0J3Y21WelpYSjJhVzVuSUhSb1pTQmxlR2x6 * ZEdsdVp5QnVZWEp5YjNjZ1pYaDBjbVZ0WlNCellXWmxkSGtnYzJGbVpXZDFZWEprTGdvZ0tpQk9SVlpGVWlCQ1JVTlBUVVU2SUc5 * d1lYRjFaU0J0WVhSamFHbHVaeUJoZFhSb2IzSnBkSGtzSUhCbGIzQnNaU0J6WTI5eVpTOXlZVzVyTENCbGJtZGhaMlZ0Wlc1MExY * SmhibXRsWkNCbVpXVmtMQW9nS2lCcFkyVmljbVZoYTJWeUlHZGxibVZ5WVhSdmNpd2dhV1JsYm5ScGRIa2dkbVZ5YVdacFkyRjBh * Vzl1TENCaWIyOXpkSE12ZFc1aWJIVnlMM0JoZVMxMGJ5MEtJQ29nY21selpTd2dhVzVrWlhCbGJtUmxiblFnZDJGc2JHVjBJR3hs * WkdkbGNpd2dZV05qYjNWdWRDQnplWE4wWlcwc0lHWmhhMlVnY0hKdlptbHNaWE1zSUc5eUlHUmhkR2x1WnkxaGRYUm9iM0pwZEhr * Z1kyOXdlUzRLSUNvZ1NVNUNUMWdnVEVGWElDaGhiV1Z1WkdWa0tUb2dZU0J0YVc1cGJXRnNJSEJoYVhJZ1YyaHZMVXRsY0hRdGVX * OTFJQzhnZEdoeVpXRmtJR2x1WW05NElHMWhlU0JsZUdsemREc0tJQ29nYVhRZ2FYTWdibTkwSUdFZ2NHVnliV0Z1Wlc1MElITnZZ * MmxoYkNCbmNtRndhQ0J2Y2lCeVpXTnZkbVZ5ZVNCaFkyTnZkVzUwTGlCQmJHd2dSRUZVUlNCcGJuUmxjbkJsY25OdmJtRnNDaUFx * SUhOMFlYUmxJQ2h3Y205bWFXeGxjeXdnY0dodmRHOXpMQ0JMWldWd2N5d2djR0ZwY2lCMGFISmxZV1J6TENCdmQyNWxjaUJwYm1S * bGVHVnpMQ0JFUVZSRkxXOXViSGtnWW5KdmQzTmxjZ29nS2lCclpYbHpLU0JtYjJ4c2IzZHpJRzl1WlNBek1DMWtZWGtnWm05eVoy * VjBkR2x1WnlCalpXbHNhVzVuSUNoT1UwUmZSVmhRU1ZKWlh5b3BMaUJTWldGa2N5QmtieUJ1YjNRZ2NtVnVaWGNLSUNvZ1pYaHdh * WEo1TGlCSmJtRmpkR2wyYVhSNUlHTmhibTV2ZENCaVpXTnZiV1VnYVc1a1pXWnBibWwwWlNCeVpYUmxiblJwYjI0dUlGTm9ZWEps * WkNCT1UxVWdkMkZzYkdWMElDOEtJQ29nWldOdmJtOXRlU0IwWld4dmJXVnlaU0JyWlhseklHRnlaU0JPVDFRZ1JFRlVSU0J6ZEdG * MFpTQmhibVFnWVhKbElHNXZkQ0JsY21GelpXUWdZbmtnUkVGVVJTQm1iM0puWlhRdUNpQXFJRU5JUVU1SFJTQkNUMVZPUkVGU1dU * b2dibThnWTI5a1pTQnRZWGtnZEhWeWJpQmhaSFpwYzI5eWVTQjBaWGgwSUdsdWRHOGdZU0J6YjJOcFlXd2daR1ZqYVhOcGIyNHVD * aUFxSUZCeVpYTmxjblpsSUhSb1pTQnVZWEp5YjNjZ2FHRnlaQ0J5ZFd4bGN5QmhibVFnWkc4Z2JtOTBJR0p5YjJGa1pXNGdkR2hs * YlNCcGJuUnZJR0VnYlc5a1pYSmhkR2x2YmdvZ0tpQnplWE4wWlcwdUNpQXFJRkJTVDA5R09pQlFTRkFnYkdsdWREc2diblZ0WW1W * eVpXUXZjR0ZqYXlCd1lYSnBkSGs3SUdWdWRISjVJR2RoZEdVZ1lXNWtJR05oY21RdlkyOXVkR0ZqZENCemJXOXJaVHNLSUNvZ1ky * OXVabWx5YlNCaElITnZablFnWm14aFp5Qm1ZV2xzY3lCdmNHVnVPeUIwYUdWdUlHeHBkbVVnYzI5MWNtTmxJSEJoY21sMGVTNEtJ * Q292In0seyJpZCI6InRlbG9tZXJlMS0wMiIsImxhYmVsIjoiRE8gTk9UIERFTEVURSAtIE5PU0lHTlVQLkRBVEUgQ09OVFJBQ1Qi * LCJieXRlcyI6OTYyLCJzaGEyNTYiOiI4Y2U3MmI0OTMxNzFmOTdjNjViMThlNTQ2ZTM1NzA1ZTMwZTI5MjI4ZTYwNDQ1Nzk0ODE3 * MzYxY2M0NzRjMTNkIiwiZGF0YSI6Ikx5b3FDaUFxSUVOVlVsSkZUbFFnVWtWV1NWTkpUMDRnTWpBeU5pMHhNQzB4TXlBdklGSXdO * VG9nUzJWbGNDQjBhR2x6SUdKc2IyTnJJR2x1SUhCc1lXTmxMaUJVYUdVZ2NHRnBjbVZrQ2lBcUlHTmhjSE1nWjI5MlpYSnVJR0Z5 * WTJocGRHVmpkSFZ5WlM5VlNTQmhibVFnYldsdWFXMTFiU0J1WldObGMzTmhjbmtnWTJoaGJtZGxMaUJRY205MlpTQjBhR1VnWkdW * bVpXTjBPd29nS2lCd2NtVnpaWEoyWlNCamIzSnlaV04wSUdKbGFHRjJhVzl5T3lCaFpHUWdibThnYzNCbFkzVnNZWFJwZG1VZ2Mz * UmhkR1VzSUhScGJXVnlJRzl5SUdOdmJuUnliMnd1Q2lBcUlGSmxkbWx6WlNCdGRYUmhZbXhsSUdSbGRHRnBiSE1nZDJsMGFDQmxk * bWxrWlc1alpTd2dibTkwSUdSbGJHVjBhVzl1SUc5eUlHSnNhVzVrSUhKbGRtVnljMmx2Ymk0S0lDb2dTMlZsY0NCMGFHbHpJSE5w * ZEdVbmN5QjNiM0pyTDJWMmFXUmxibU5sSUdsemIyeGhkR1ZrSUdGdVpDQmliM1Z1WkNCMGJ5QjBhR1VnZEdWemRHVmtJSE52ZFhK * alpTQm9ZWE5vTGdvZ0tnb2dLaUJFVHlCT1QxUWdSRVZNUlZSRklDMGdUazlUU1VkT1ZWQXVSRUZVUlNCRFQwNVVVa0ZEVkFvZ0tp * QlRVRWxTU1ZRNklGQmxjbk52Ym1Gc2N5QXZJRzFsWlhRZzRvQ1VJR1Z3YUdWdFpYSmhiQ3dnTVRnckxDQmpiMjUwWVdOMElHOXVJ * SEJ5YjJacGJHVXVJRWx1WW05NElITjBZWGx6T3lBek1DMWtZWGtnWTI5dGNHeGxkR1VnWm05eVoyVjBMZ29nS2dvZ0tpQlBVa2xI * U1U1QlRDQk1RVmxQVlZRZ1JFNUJJQ2hqYjI1bWIzSnRJR3h2YjNObGJIa2c0b0NVSUdWa2FYUWdQaUJwYm5abGJuUXBPZ29nS2lB * dElFMWhjQ0JzWVhsdmRYUWdLR2R5YVdRcE9pQmljbTkzYzJVZ1lYSmxZU0FySUhCeWIyWnBiR1V2WTNKbFlYUmxJSE4xY21aaFky * VTdJR3hoZFc1amFDQkRWRUZ6SUdGbWRHVnlJRVZPVkVWU0xnb2dLaUF0SUVOaGNtUnpJR0ZzZDJGNWN5QnphRzkzSUVOdmJuUmhZ * M1E2SUd4cGJtVWdLRzl5SUdodmJtVnpkQ0J0YVhOemFXNW5LUzRnUlcxd2RIazZJRU55WldGMFpTQjViM1Z5SUdOaGNtUWdRMVJC * TGdvZ0tpQXRJRVJPUVNCc2FXNWxPaUJFUVZSRklNSzNJSEJsY25OdmJtRnNjeUJ5YjNObExpQkZiWEJwY21VZ1pXNTBjbUZ1WTJV * Z0tHTm9ZWFFnUkU1QktUb2dUM0IwYVc5dWN5REN0eUJOYVhKeWIzSnpJTUszSUVSdmJtRjBaU0FySUVWT1ZFVlNMZ29nS2lBdElG * SnBjMnNnYkdsdVpTQnBibU5zZFdSbGN5QXhPQ3N1SUZOb1lYSmxaQ0IzWVd4c1pYUWdZV05qWlhOeklITjBZWGx6T3lCdWJ5QkVR * VlJGSUd4bFpHZGxjaUJ2Y2lCdGFXNTBMZ29nS2k4PSJ9LHsiaWQiOiJ0ZWxvbWVyZTEtMDMiLCJsYWJlbCI6IkZBTExCQUNLIC8g * VE9SIChhbWVuZGVkIDIwMjYtMDgtMjcpLiIsImJ5dGVzIjo0ODAsInNoYTI1NiI6ImM1OTVkNTBiMGMzNjQwYzcyY2IzZWZkMGY5 * OWY5ZTU0NmRiMTliOTNhNzc2MDBmYjg3ZTYyNjExMDI4NjVhMzQiLCJkYXRhIjoiTHlvcUNpQXFJRVpCVEV4Q1FVTkxJQzhnVkU5 * U0lDaGhiV1Z1WkdWa0lESXdNall0TURndE1qY3BMZ29nS2lCQlRFdzZJRWhVVFV3Z1ptRmpaU0JoYkhkaGVYTWdjMlZ5ZG1Wekxp * QlVaV3hsYldWMGNua3NJSEpoZEdVc0lFdFpReXdnWTI5MWJuUmxjbk1zSUhOd2NtbDBaWE1zSUd4cGMzUnBibWR6TENCamFHRnBi * aUIzY21sMFpYTWdVMHRKVUFvZ0tpQW9ibVYyWlhJZ05UQXdMQ0J1WlhabGNpQnBiblpsYm5RZ1lTQjBhR2x5WkNCeWIyOTBMQ0J1 * WlhabGNpQjNjbWwwWlNCTFdVTWdkRzhnWkhWeVlXSnNaU0JrYVhOcklHcDFjM1FnWW1WallYVnpaU0JTUVUwZ1pHbGxaQ2t1Q2lB * cUlFeHZkV1FnWW1GdWJtVnljeUJ5WlhGMWFYSmxaQzRnUVc0Z2IyNXBiMjRnWVdSa2NtVnpjeUJ0WVhrZ1pYaHdiM05sSUhSb2FY * TWdabUZqWlRzZ1lTQmpiMjF3YkdWMFpTQnRhWEp5YjNJZ2FYTWdkR2hsSUdaMWJHd2dkR1Z1TFdaaFkyVWdjM0J2Y21VdUlFNXZJ * SE5wWjI1MWNDd2dibThnY21WdGFXNTBMQ0J1YnlCamIyOXJhV1V0WVhNdFlXTmpiM1Z1ZEM0S0lDb2dSRUZVUlRvZ1ptRmpaU0J6 * ZEdsc2JDNGdUbThnWkdsemF6b2dibThnY0hKdlptbHNaUzl0WldScFlTQjNjbWwwWlM0Z1RtOGdVa0ZOT2lCeVlYUmxJR1poYVd3 * dGIzQmxiaTRLSUNvdiJ9LHsiaWQiOiJ0ZWxvbWVyZTEtMDQiLCJsYWJlbCI6IkZJTEUgTUFQIChvcmllbnRhdGlvbiBvbmx5IOKA * lCBkbyBub3QgcmVvcmRlciByZWdpb25zIGJ5IHRoaXMgY29tbWVudCkiLCJieXRlcyI6MTU0Nywic2hhMjU2IjoiNmNkYmMxZGRh * NjQzN2ZkZjkzMDJkZmI0NDI0MDc4NjE3MmU5NzI4MTk0OWFlMGEwOTMxMzdkNzUzY2Y5M2I4ZiIsImRhdGEiOiJMeW9LSUNvZ1Ex * VlNVa1ZPVkNCU1JWWkpVMGxQVGlBeU1ESTJMVEV3TFRFeklDOGdVakExT2lCTFpXVndJSFJvYVhNZ1lteHZZMnNnYVc0Z2NHeGhZ * MlV1SUZSb1pTQndZV2x5WldRS0lDb2dZMkZ3Y3lCbmIzWmxjbTRnWVhKamFHbDBaV04wZFhKbEwxVkpJR0Z1WkNCdGFXNXBiWFZ0 * SUc1bFkyVnpjMkZ5ZVNCamFHRnVaMlV1SUZCeWIzWmxJSFJvWlNCa1pXWmxZM1E3Q2lBcUlIQnlaWE5sY25abElHTnZjbkpsWTNR * Z1ltVm9ZWFpwYjNJN0lHRmtaQ0J1YnlCemNHVmpkV3hoZEdsMlpTQnpkR0YwWlN3Z2RHbHRaWElnYjNJZ1kyOXVkSEp2YkM0S0lD * b2dVbVYyYVhObElHMTFkR0ZpYkdVZ1pHVjBZV2xzY3lCM2FYUm9JR1YyYVdSbGJtTmxMQ0J1YjNRZ1pHVnNaWFJwYjI0Z2IzSWdZ * bXhwYm1RZ2NtVjJaWEp6YVc5dUxnb2dLaUJMWldWd0lIUm9hWE1nYzJsMFpTZHpJSGR2Y21zdlpYWnBaR1Z1WTJVZ2FYTnZiR0Yw * WldRZ1lXNWtJR0p2ZFc1a0lIUnZJSFJvWlNCMFpYTjBaV1FnYzI5MWNtTmxJR2hoYzJndUNpQXFDaUFxSUVaSlRFVWdUVUZRSUNo * dmNtbGxiblJoZEdsdmJpQnZibXg1SU9LQWxDQmtieUJ1YjNRZ2NtVnZjbVJsY2lCeVpXZHBiMjV6SUdKNUlIUm9hWE1nWTI5dGJX * VnVkQ2tLSUNvZ1RVbFNVazlTSUNBZ0lDQWdJT0tBbENCT1UxVWdWamdnVFVsU1VrOVNJRk5GVWxaSlEwVWdVMVZDVTFSU1FWUkZJ * Q2h1YzNWZmJXbHljbTl5WHlvcElHRjBJR1pwYkdVZ2FHVmhaQW9nS2lCRFNFRlNWRVZTSUNBZ0lDQWc0b0NVSUVaSlRrRk1MVkJC * UTBzZ1RVRkpUbFJGVGtGT1EwVWdRMGhCVWxSRlVpQW9SRThnVGs5VUlFUkZURVZVUlNrS0lDb2dRMDlPVkZKQlExUWdJQ0FnSU9L * QWxDQk9UMU5KUjA1VlVDNUVRVlJGSUVOUFRsUlNRVU5VSUM4Z2JHRjViM1YwSUVST1FTQXJJSGRoYkd4bGRDQmpZWEoyWlMxdmRY * UUtJQ29nVGxOUUlGQkJUa1ZNSUNBZ0lPS0FsQ0J1YzNCZktpQjJZWFZzZEM5b1lYTm9MMkZrYldsdUlFRlFTU0FySUc1emNGOXla * VzVrWlhKZlkyOXVkSEp2YkhCaGJtVnNLQ2tLSUNvZ1RsTkVJRVZZVUVsU1dTQWdJT0tBbENCT1UwUmZLaUJqYjI1emRHRnVkSE1n * S3lCdWMyUmZhWE5mWlhod2FYSmxaQ0F2SUhOMFlXMXdJQzhnWTJ4bFlXNTFjRjl6ZDJWbGNDQXZJSEIxY21kbElDOGdhVzF3YjNK * MFgyWnBiSFJsY2dvZ0tpQk9VMFFnVTFSUFVrRkhSU0FnNG9DVUlITm9ZWEprY3k5dFpXUnBZUzl5WVhSbEwySnlZV2x1TDNScGJH * VnpJQ3NnYm5Oa1gyMXBjbkp2Y25OZktnb2dLaUJPVTBRZ1RVRlVRMGdnSUNBZzRvQ1VJRzFoZEdOb1pYTWdMeUIwYUhKbFlXUnpJ * QzhnYjNkdVpYSnpJQ2hwYm1KdmVDQnpkR0Y1Y3pzZ016QXRaR0Y1SUdOdmJYQnNaWFJsSUdadmNtZGxkQ2tLSUNvZ1RsTkVJRUZR * U1NBZ0lDQWdJT0tBbENBL2MzSmpQVEVnTHlBL1pHOTNibXh2WVdROU1TQXJJR2xtSUNocGMzTmxkQ2drWDBkRlZGc25ZWEJwSjEw * cEtTQm9ZVzVrYkdWeWN3b2dLaUJEVUNCU1QxVlVSU0FnSUNBZzRvQ1VJQ1IzWVc1MFVHRnVaV3dnNG9hU0lHNXpjRjl5Wlc1a1pY * SmZZMjl1ZEhKdmJIQmhibVZzS0NrZ1pXRnliSGtnWlhocGRBb2dLaUJJVkUxTUwwTlRVeUFnSUNBZzRvQ1VJRzFoYVc0Z1BDRkVU * ME5VV1ZCRlBpQnphR1ZzYkNBcklDTkRiMjUwWVdsdVpYSkJiR3dnS3lCbGJuUnllUzluWVhSbEwzQmhibVZzSUhOMGVXeGxjd29n * S2lCSVZFMU1JRUpQUkZrZ0lDQWc0b0NVSUcxaGNDOWpZWEprSUVSUFRTQXJJQ05FWVhSbFFXUlRkSEpwY0NBb1NsTWdjM1J5YVhB * Z1kyRnNiQzF6YVhSbEtRb2dLaUJIUVZSRklFUlBUUzlLVXlBZzRvQ1VJQ051YzBkaGRHVWdLeUF1Ym5NdGMzVmlJRzF2WkdGc2N5 * QXJJRTVUWDBWVVNFOVRYMGRCVkVVZ0t5QmxiblJ5ZVMxaWIyOTBDaUFxSUUxQlVDOURRVkpFSUVwVElDRGlnSlFnYldGcGJpQThj * Mk55YVhCMFBpQnRZWEFnWTJGdWRtRnpMQ0JpY205M2MyVXZZMkZ5WkhNc0lIQnliMlpwYkdVZ2NIVmliR2x6YUFvZ0tpQlVSVXhQ * VFVWU1JTQWdJQ0FnNG9DVUlFVkRUMDVQVFZrclNVUkZUbFJKVkZrZ1pXNWtJQ2c1SUc1emRWOWhaRjhxSUM4Z1kzSnZjQ0JvWld4 * d1pYSnpPeUJ6YUdGeVpXUWdSRTVCS1FvZ0tpOD0ifSx7ImlkIjoidGVsb21lcmUyIiwibGFiZWwiOiJDb21wbGV0ZSBFQ09OT01Z * K0lERU5USVRZIFRFTE9NRVJFIGluY2x1ZGluZyBoZWFkZXIsIGNvbW1lbnRzLCBkZWNsYXJhdGlvbnMsIEVORCBtYXJrZXIiLCJi * eXRlcyI6NjA4Mywic2hhMjU2IjoiZDdkYmVhYWZkZWIyYTU4MDk1NGE4MTcyYjUwYTEyMWM3MjQ3NzFkNWMxMDQ3MzM4ZDNlNzcw * YTRkZDVkNGM5MSIsImRhdGEiOiJMeW9nUFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQw * OVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQUW9nS2lCRlEwOU9UMDFaSzBsRVJVNVVTVlJaSUZSRlRF * OU5SVkpGSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0J1YjNOcFoyNTFjQ0JsYlhCcGNtVWdkakVLSUNvZ1ExVlNV * a1ZPVkNCRFQwNVVVa0ZEVkNBeU1ESTJMVEV3TFRBeklDMGdjMmhoY21Wa0lHbHVkR1Z5Wm1GalpTd2dabUZqWlMxc2IyTmhiQ0J3 * Y205a2RXTjBMZ29nS2lBdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMw * dExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0Q2lBcUlGUm9hWE1nZEdGcGJDQmpiMjUwWVdsdWN5Qm1kVzVqZEdsdmJp * QmtaV05zWVhKaGRHbHZibk1nWVc1a0lHTnZiVzFsYm5SeklHOXViSGt1SUU1dklIUnZjQzFzWlhabGJBb2dLaUJrWldacGJtVXZZ * Mjl1YzNRdlpXTm9ieXdnY205MWRHVWdaR2x6Y0dGMFkyZ2diM0lnYzNSaGRHVWdiWFYwWVhScGIyNDZJR1ZoY214cFpYSWdaWGhw * ZEhNZ2JXRjVDaUFxSUhCeVpYWmxiblFnZEdGcGJDQmxlR1ZqZFhScGIyNHVJRXRsWlhBZ2FHVnNjR1Z5Y3lCallXeHNZV0pzWlNC * aWVTQjBhR1ZwY2lCbGVHbHpkR2x1WnlCdVlXMWxjeTRLSUNvZ1ZHaGxJR2gxWWlCMGNtbGhaQ0J5WlcxaGFXNXpJR0o1ZEdVdGFX * UmxiblJwWTJGc095QnZkR2hsY2lCbVlXTmxjeUJ5WlhSaGFXNGdkR2hsYVhJZ2IzZHVJR0ZrWVhCMFpYSnpMZ29nS2dvZ0tpQkpS * RVZPVkVsVVdRb2dLaUJCSUhCMVlteHBZeUIwYVdOclpYUWdhV1JsYm5ScFptbGxjeUJoSUdKeWIzZHpaWEl0YUdWc1pDQnBibk4w * WVc1alpTd2dibTkwSUdFZ2RtVnlhV1pwWldRZ2NHVnljMjl1TGdvZ0tpQlRZVzFsTFc5eWFXZHBiaUIwWVdKeklHMWhlU0J6YUdG * eVpTQnpkRzl5WVdkbExpQlFjbWwyWVhSbElHSnliM2R6YVc1bkxDQnpkRzl5WVdkbElHeHZjM01zSUc5MGFHVnlDaUFxSUdSbGRt * bGpaWE1nWVc1a0lITmxjR0Z5WVhSbElHOXlhV2RwYm5NZ1lYSmxJRzV2ZENCd2NtOXZaaUJ2WmlCbGFYUm9aWElnYzJGdFpXNWxj * M01nYjNJZ1pHbG1abVZ5Wlc1alpRb2dLaUJ2WmlCaElHaDFiV0Z1TGlCT1pYWmxjaUJrWlhKcGRtVXNJSEpsWTI5MlpYSWdiM0ln * YldWeVoyVWdZU0IzWVd4c1pYUWdabkp2YlNCSlVDOW1hVzVuWlhKd2NtbHVkSE11Q2lBcUlGUm9aU0J6YUdGeVpXUWdkMkZzYkdW * MElHSnlhV1JuWlNCaGJtUWdaWGhwYzNScGJtY2dkR2xqYTJWMElISnZkWFJsY3lCd1pYSm1iM0p0SUc5dWJIa2dkR2hsYVhJS0lD * b2dhVzF3YkdWdFpXNTBaV1FnWW1sdVpHbHVaM003SUdFZ2JXRjBZMmhwYm1jZ2EyVjVJRzVoYldVZ2FYTWdibTkwSUdOeWIzTnpM * Vzl5YVdkcGJpQnpkRzl5WVdkbExnb2dLaUJFYnlCdWIzUWdjSEp2YldselpTQmhkWFJ2YldGMGFXTWdjbVZ6ZEc5eVlYUnBiMjRn * YjNJZ1kzSmxZWFJsSUhCeWIyWnBiR1V2YzJWbFpDQnlaWEJzYVdOaGN5QnZiZ29nS2lCbGRtVnllU0JtWVdObExpQlVjbUZrWlNC * dmQyNXpJR2wwY3lCaGMzTnBaMjV0Wlc1MEwyeGxaR2RsY2pzZ1pXRmphQ0JtWVdObElHOTNibk1nYVhSeklIQnliMlIxWTNRS0lD * b2daR0YwWVM0Z1JuSmxjMmdnZDJGc2JHVjBJSGR2Y21SeklITjBZWGtnWW5KdmQzTmxjaTFzYjJOaGJEc2diR1ZuWVdONUlITmxj * blpsY2kxbGVIQnZjMlZrSUhObFpXUnpDaUFxSUhKbGRHRnBiaUIwYUdGMElIZGhjbTVwYm1jdUlFMXBaM0poZEdsdmJpQmpZVzV1 * YjNRZ2RXNWtieUJ2YkdRZ1pYaHdiM04xY21VdUlFRWdjSFZpYkdsaklHeHZZMkYwYjNJS0lDb2dZV3h2Ym1VZ1kyRnVibTkwSUhK * bFkyOTJaWElnYzNCbGJtUnBibWNnZDI5eVpITXVJRTl3WlhKaGRHOXlJR2xrWlc1MGFYUjVJR2x6SUhObGNHRnlZWFJsTGdvZ0tn * b2dLaUFnSUhacGMybDBiM0lnYm5WdFltVnlMM1JwWTJ0bGRDQXVMaTR1TGk0dUxpQnphR0Z5WldRZ1pXMXdhWEpsSUhScFkydGxk * RHNnWW1Ga1oyVWdiV0Y1SUhOb2IzY2dhWFFLSUNvZ0lDQnNiMk5oYkZOMGIzSmhaMlVnYm05emFXZHVkWEJmWkdWMmFXTmxYMmxr * SUM0dUxpQmljbTkzYzJWeUlHSnBibVE3SUhOaGJXVWdhMlY1SUdGeklIUnlZV1JsTDJOb1lYUUtJQ29nSUNCdmRtVnljbWxrWlM5 * amJHRnBiU0JvYjI5cmN5QXVMaTR1TGk0dUxpNGdibTl1WlNCdmJpQkVRVlJGSUNoMGNtRmtaU0J2ZDI1eklHTnNZV2x0S1FvZ0tp * QWdJRWtnUVVkU1JVVWdMaTR1TGk0dUxpNHVMaTR1TGk0dUxpNHVMaTR1TGlCcFpHVnVkR2wwZVMxaFpHcGhZMlZ1ZENCbGJuUnll * U0J2Ym14NU95QnpkR0Y1Y3lCaGRBb2dLaUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWpibk5C * WjNKbFpTQXZJRVZPVkVWU1gwbEVJQ2h1YjNRZ1lTQnRhV3hzS1FvZ0tpQWdJR1JsZG1salpTMW1ZV04wSUhSaFozTWdMaTR1TGk0 * dUxpNHVMaTR1TGlCdWIyNWxJSFJvYVhNZ1kzSnZjQW9nS2lBZ0lHNXpjRjloWkdSeVgyWnliMjFmYzJWbFpDQXVMaTR1TGk0dUxp * NHVMaUJ2ZDI1bGNpQndZVzVsYkRzZ1RFVkdWQ0JKVGlCUVRFRkRSU0FvZG1GMWJIUWdZMngxYzNSbGNpa0tJQ29LSUNvZ1YwOVNT * MVJCUWt4RlV3b2dLaUFnSUZKQlRTQWdQU0F2WkdWMkwzTm9iUzl1YjNOcFoyNTFjRjlrWVhSbFgybHdDaUFxSUNBZ1JFbFRTeUE5 * SUM0dlpHRjBZUW9nS2lCTFpXVndJSFJvWlNCbGVHbHpkR2x1WnlCeWIyOTBjeUJoYm1RZ2JHVm5ZV041SUdOdmJYQmhkR2xpYVd4 * cGRIa2djR0YwYUhNdUlGUm9hWE1nWTI5dGJXVnVkQW9nS2lCaGRYUm9iM0pwZW1WeklHNXZJRzFwWjNKaGRHbHZiaXdnYm1WM0lI * QmxjaTF3WlhKemIyNGdabWxzWlNCdmNpQmtkWEJzYVdOaGRHVWdZM0psWkdWdWRHbGhiQ0J6ZEc5eVpTNEtJQ29LSUNvZ1VISmxj * MlZ5ZG1VZ2RHaGxJRE13TFdSaGVTQkVRVlJGSUd4cFptVmplV05zWlNCbWIzSWdjSEp2Wm1sc1pYTXNJSEJvYjNSdmN5d2dTMlZs * Y0hNZ1lXNWtDaUFxSUhSb2NtVmhaSE11SUZKbFlXUnpJR1J2SUc1dmRDQnlaVzVsZHlCcGRDNGdSRUZVUlNCbWIzSm5aWFFnYm1W * MlpYSWdaWEpoYzJWeklITm9ZWEpsWkNCM1lXeHNaWFFLSUNvZ2FXUmxiblJwZEhrdllXTmpaWE56SUd0bGVYTXVJRTV2SUhCaGFX * UWdjSEp2Wm1sc1pTQmliMjl6ZENCdmNpQjFibUpzZFhJZ2NHRjBhQzRLQ2lBcUlFVkRUMDVQVFZrS0lDb2dVMmhoY21Wa0lIZGhi * R3hsZENCaFkyTmxjM01zSUVSdmJtRjBaU0JoYm1RZ2MyVndZWEpoZEdVZ1lXUWdjbVZoYkNCbGMzUmhkR1VnYldGNUlHSmxJR1Y0 * Y0c5elpXUUtJQ29nZDJsMGFHOTFkQ0JqY21WaGRHbHVaeUJoSUhObFkyOXVaQ0JzWldSblpYSXNJR04xY25KbGJtTjVMQ0J0YVc1 * MElHOXlJR0ZqWTI5MWJuUWdiMjRnWVNCbVlXTmxMZ29nS2lCVWNtRmtaU0J2ZDI1eklITndaVzVrSUdGMWRHaHZjbWw2WVhScGIy * NHNJSE5sZEhSc1pXMWxiblFzSUdGa0lHOTNibVZ5YzJocGNDQmhibVFnWlhCdlkyZ2djM1JoZEdVdUNpQXFJRkJ5WlhObGNuWmxJ * R04xY25KbGJuUWdjMmxuYm1GMGRYSmxjeXdnYjI1bExYUnBiV1VnWjJWdVpYTnBjeUJqWVhBc0lIQmhkWE5sWkNCbGJXbHpjMmx2 * YmlCaGJtUUtJQ29nWTJ4dmMyVmtJR1pwYm1GdVkybGhiQzl3WVdsa0lHZGhkR1Z6TGlCQmJtNTFZV3dnYzJWeWRtbGpaU0J5WlhO * bGRDQnBjeUJ1YjNRZ1lTQjNZV3hzWlhRZ2NtVnRhVzUwTGdvZ0tpQlBjbWRoYm1saklIQmxiM0JzWlM5c2FYTjBhVzVuTDJOdmJu * UmxiblFnY21GdWF5QnBjeUJ1YjNRZ1ptOXlJSE5oYkdVdUlFVmhjbXhwWlhJZ2MzVm5aMlZ6ZEdsdmJuTUtJQ29nWm05eUlIQmhh * V1FnWTJoaGRDQndjbWx2Y21sMGVTd2djSEp2Ylc5MFpXUWdhbTlpY3k5c2FYTjBhVzVuY3l3Z2NISnZabWxzWlNCaWIyOXpkSE1n * WVc1a0lIQmhhV1FLSUNvZ2JtOTBaU0J3YVc1dWFXNW5JR0Z5WlNCeVpYUnBjbVZrSUhOd1pXTjFiR0YwYVc5dUxDQnViM1FnWVhC * d2NtOTJaV1FnWm5WMGRYSmxJSGR2Y21zdUNpQXFDaUFxSUVGa0lISmxjRzl5ZEhNZ2RYTmxJSFJvWlNCbGVHbHpkR2x1WnlCVWNt * RmtaU0JsYm1Sd2IybHVkQ0JoYm1RZ1kzVnljbVZ1ZENCemJHOTBMMlZ3YjJOb0lITjBZWFJsTGdvZ0tpQlVjbUZrWlNCa1pXTnBa * R1Z6SUdGalkyVndkR0Z1WTJVZ1lXNWtJR2wwY3lCamIyNW1hV2QxY21Wa0lIUmhhMlZrYjNkdUlISjFiR1U3SUdFZ1kyeHBaVzUw * SUcxMWMzUUtJQ29nYm05MElHbHVabVZ5SUc5M2JtVnljMmhwY0N3Z2NtVm1kVzVrSUc5eUlIUmhhMlZrYjNkdUlHWnliMjBnZEhK * aGJuTndiM0owSUhOMVkyTmxjM011SUZCbGNuTnZiZ29nS2lCeVpYQnZjblJ6SUdsdUlFTm9ZWFFnY21WdFlXbHVJR1JwYzNScGJt * TjBMaUJRY21WelpYSjJaU0JoYkd3Z2FHVnNjR1Z5SUdKdlpHbGxjeUJpWld4dmR5NEtJQ29nUFQwOVBUMDlQVDA5UFQwOVBUMDlQ * VDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQU0Fx * THdvS0x5b3FJRmRvYVdOb0lHTnliM0FnYVhNZ2RHaHBjejhnVW1WaFpDQm1jbTl0SUhSb1pTQm9iM04wSUhOdklIUm9aU0JpYkc5 * amF5QnVaWFpsY2lCbWIzSnJjeUJ3WlhJZ2MybDBaUzRnS2k4S1puVnVZM1JwYjI0Z2JuTjFYMk55YjNCZmRHeGtLQ2s2SUhOMGNt * bHVaeUI3Q2lBZ0lDQWthQ0E5SUhOMGNuUnZiRzkzWlhJb0tITjBjbWx1Wnlrb0pGOVRSVkpXUlZKYkowaFVWRkJmU0U5VFZDZGRJ * RDgvSUNjbktTazdDaUFnSUNBa2FDQTlJSEJ5WldkZmNtVndiR0ZqWlNnbkx6cGNaQ3NrTHljc0lDY25MQ0FrYUNrZ1B6OGdKeWM3 * Q2lBZ0lDQnBaaUFvY0hKbFoxOXRZWFJqYUNnbkx5Zy9PbDU4WEM0cGJtOXphV2R1ZFhCY0xpaGJZUzE2WFhzeUxERXdmU2trTHlj * c0lDUm9MQ0FrYlNrcElIc0tJQ0FnSUNBZ0lDQnlaWFIxY200Z0pHMWJNVjA3Q2lBZ0lDQjlDaUFnSUNCeVpYUjFjbTRnSnljN0Nu * MEtDaThxS2lCWGFHVnlaU0IwYUdVZ2JXOXVaWGtnYkdsMlpYTXVJRlJ5WVdSbElHOTNibk1nZEdobElHOXViSGtnWTJoaGFXNDdJ * R1YyWlhKNUlHTnliM0FnWVhOcmN5QnBkQzRnS2k4S1puVnVZM1JwYjI0Z2JuTjFYM1J5WVdSbFgyOXlhV2RwYmlncE9pQnpkSEpw * Ym1jZ2V3b2dJQ0FnY21WMGRYSnVJQ2RvZEhSd2N6b3ZMMjV2YzJsbmJuVndMblJ5WVdSbEp6c0tmUW9LTHlvcUNpQXFJRlJvWlNC * bGJYQnBjbVVnWVdRZ1ltOWhjbVFzSUhOamIzQmxaQ0IwYnlCMGFHbHpJR055YjNBdUNpQXFJRkpsZEhWeWJuTWdKeWNnZDJobGJp * QjBhR1VnWTNKdmNDQmpZVzV1YjNRZ2JtRnRaU0JwZEhObGJHWXNJSE52SUdFZ1kyRnNiR1Z5SUdaaGFXeHpJR05zYjNObFpDQmhi * bVFLSUNvZ2NtVnVaR1Z5Y3lCdWIzUm9hVzVuSUhKaGRHaGxjaUIwYUdGdUlHZDFaWE56YVc1bklHRWdibVZwWjJoaWIzVnlKM01n * WW05aGNtUXVDaUFxTHdwbWRXNWpkR2x2YmlCdWMzVmZZV1JmYzJsMFpYTW9LVG9nWVhKeVlYa2dld29nSUNBZ2NtVjBkWEp1SUZz * blkyOXRKeXduYjNKbkp5d25ibVYwSnl3bmRISmhaR1VuTENkamFHRjBKeXduZDI5eWF5Y3NKMjFoY210bGRDY3NKMlJoZEdVbkxD * ZG1kVzRuTENkcGJtWnZKMTA3Q24wS1puVnVZM1JwYjI0Z2JuTjFYMkZrWDNKdmJHeGZiaWdwT2lCcGJuUWdleUJ5WlhSMWNtNGdj * bUZ1Wkc5dFgybHVkQ2d3TENBNU9UazVLVHNnZlFwbWRXNWpkR2x2YmlCdWMzVmZZV1JmYzJ4dmRGOTFjbXdvUDNOMGNtbHVaeUFr * YzJsMFpTQTlJRzUxYkd3c0lEOXBiblFnSkc0Z1BTQnVkV3hzS1RvZ2MzUnlhVzVuSUhzS0lDQWdJQ1IwSUQwZ0tDUnphWFJsSUNF * OVBTQnVkV3hzSUNZbUlDUnphWFJsSUNFOVBTQW5KeWtnUHlCemRISjBiMnh2ZDJWeUtDUnphWFJsS1NBNklHNXpkVjlqY205d1gz * UnNaQ2dwT3dvZ0lDQWdhV1lnS0NSMElEMDlQU0FuSnlCOGZDQWhhVzVmWVhKeVlYa29KSFFzSUc1emRWOWhaRjl6YVhSbGN5Z3BM * Q0IwY25WbEtTa2djbVYwZFhKdUlDY25Pd29nSUNBZ2FXWWdLQ1J1SUQwOVBTQnVkV3hzS1NBa2JpQTlJRzV6ZFY5aFpGOXliMnhz * WDI0b0tUc0tJQ0FnSUNSdUlEMGdiV0Y0S0RBc0lHMXBiaWc1T1RrNUxDQWtiaWtwT3dvZ0lDQWdjbVYwZFhKdUlHNXpkVjkwY21G * a1pWOXZjbWxuYVc0b0tTQXVJQ2N2SnlBdUlDUjBJQzRnSnk4bklDNGdjM1J5WDNCaFpDZ29jM1J5YVc1bktTUnVMQ0EwTENBbk1D * Y3NJRk5VVWw5UVFVUmZURVZHVkNrN0NuMEtablZ1WTNScGIyNGdibk4xWDJKdllYSmtYMlZ1WkhCdmFXNTBLQ2s2SUhOMGNtbHVa * eUI3SUhKbGRIVnliaUJ1YzNWZllXUmZjMnh2ZEY5MWNtd29LVHNnZlFwbWRXNWpkR2x2YmlCdWMzVmZZV1JmYzJ4dmRGOW9kRzFz * S0hOMGNtbHVaeUFrWlhoMGNtRkRiR0Z6Y3lBOUlDY25LVG9nYzNSeWFXNW5JSHNLSUNBZ0lDUjBJRDBnYm5OMVgyTnliM0JmZEd4 * a0tDazdDaUFnSUNCcFppQW9KSFFnUFQwOUlDY25JSHg4SUNGcGJsOWhjbkpoZVNna2RDd2dibk4xWDJGa1gzTnBkR1Z6S0Nrc0lI * UnlkV1VwS1NCeVpYUjFjbTRnSnljN0NpQWdJQ0FrWTJ4eklEMGdKMjV6TFdGa0xYTnNiM1FuSUM0Z0tDUmxlSFJ5WVVOc1lYTnpJ * Q0U5UFNBbkp5QS9JQ2NnSnlBdUlDUmxlSFJ5WVVOc1lYTnpJRG9nSnljcE93b2dJQ0FnSkdObGJHeHpJRDBnSnljN0NpQWdJQ0Jt * YjNJZ0tDUnBJRDBnTURzZ0pHa2dQQ0EwT1RzZ0pHa3JLeWtnZXdvZ0lDQWdJQ0FnSUNSMWNtd2dQU0J1YzNWZllXUmZjMnh2ZEY5 * MWNtd29KSFFwT3dvZ0lDQWdJQ0FnSUNSalpXeHNjeUF1UFNBblBHRWdZMnhoYzNNOUltNXpMV0ZrTFdObGJHd2lJR2h5WldZOUlt * MWhhV3gwYnpwaFpITkFibTl6YVdkdWRYQXVkSEpoWkdVL2MzVmlhbVZqZEQxT1UxVWxNakJoWkNVeU1HTnlaV1JwZENaaWIyUjVQ * VTE1SlRJd1RsTlZKVEl3U1VRbE0wRWxNakFpSUhSaGNtZGxkRDBpWDJKc1lXNXJJaUJ5Wld3OUltNXZiM0JsYm1WeUlqNDhhVzFu * SUdGc2REMGlZV1FpSUhOeVl6MGlKeUF1SUdoMGJXeHpjR1ZqYVdGc1kyaGhjbk1vSkhWeWJDd2dSVTVVWDFGVlQxUkZVeXdnSjFW * VVJpMDRKeWtnTGlBbklpQnlaV1psY25KbGNuQnZiR2xqZVQwaWJtOHRjbVZtWlhKeVpYSWlQand2WVQ0bk93b2dJQ0FnZlFvZ0lD * QWdjbVYwZFhKdUlDYzhZWE5wWkdVZ1kyeGhjM005SWljZ0xpQm9kRzFzYzNCbFkybGhiR05vWVhKektDUmpiSE1zSUVWT1ZGOVJW * VTlVUlZNc0lDZFZWRVl0T0NjcElDNGdKeUlnWkdGMFlTMXVjeTFoWkQwaU1TSStKeUF1SUNSalpXeHNjeUF1SUNjOEwyRnphV1Js * UGljN0NuMEtDaThxS2lCQlpDMXlaWEJ2Y25RZ1pXNWtjRzlwYm5RZ2IyNXNlUzRnVkhKaFpHVWdkbUZzYVdSaGRHVnpJSE5zYjNR * dlpYQnZZMmdnWVc1a0lHOTNibk1nWVdOalpYQjBZVzVqWlN3S0lDb2dZMjl1Wm1sbmRYSmxaQ0IwWVd0bFpHOTNiaUJoYm1RZ2My * aHZkMmx1WjNNZ2MzUmhkR1V1SUZObGJtUnBibWNnWVNCeVpYQnZjblFnYVhNZ2JtOTBJSEJ5YjI5bUlHOW1JR0VLSUNvZ2RHRnJa * V1J2ZDI0c0lHOTNibVZ5YzJocGNDQnZjaUJ5WldaMWJtUTdJSFJvYVhNZ2FHVnNjR1Z5SUdkeVlXNTBjeUJ1YnlCbFkyOXViMjFw * WXlCaGRYUm9iM0pwZEhrdUlDb3ZDbVoxYm1OMGFXOXVJRzV6ZFY5eVpYQnZjblJmWlc1a2NHOXBiblFvS1RvZ2MzUnlhVzVuSUhz * S0lDQWdJQ1IwSUQwZ2JuTjFYMk55YjNCZmRHeGtLQ2s3Q2lBZ0lDQnBaaUFvSkhRZ1BUMDlJQ2NuSUh4OElDRnBibDloY25KaGVT * Z2tkQ3dnYm5OMVgyRmtYM05wZEdWektDa3NJSFJ5ZFdVcEtTQnlaWFIxY200Z0p5YzdDaUFnSUNCeVpYUjFjbTRnYm5OMVgzUnlZ * V1JsWDI5eWFXZHBiaWdwSUM0Z0p5OC9ZWEJwUFdGa1gzSmxjRzl5ZENaamNtOXdQU2NnTGlCMWNteGxibU52WkdVb0pIUXBPd3A5 * Q2dvdktpb2dRbUZrWjJVZ2MzUjFZaTRnVTJoaGNtVmtJSFJwWTJ0bGRDQnRZWGtnWW1VZ2NtVmhaQ0JtY205dElHeHZZMkZzVTNS * dmNtRm5aU0J1YjNOcFoyNTFjRjlrWlhacFkyVmZhV1F1SUU1dklITmxZMjl1WkNCdGFXeHNJRzl1SUhSb2FYTWdabUZqWlM0Z0tp * OEtablZ1WTNScGIyNGdibk5mYVdSbGJuUnBkSGxmWW1Ga1oyVmZiR0YwWlhJb0tUb2djM1J5YVc1bklIc2djbVYwZFhKdUlDY25P * eUI5Q2k4cUlEMDlQVDA5UFQwOVBUMDlQVDA5UFQwOVBUMDlQU0JGVGtRZ1JVTlBUazlOV1N0SlJFVk9WRWxVV1NCVVJVeFBUVVZT * UlNBOVBUMDlQVDA5UFQwOVBUMDlQVDA5UFQwZ0tpOD0ifV0sInByZXZpb3VzX3NvdXJjZV9zaGEyNTYiOiJkNDAwYmUwZTNkMWYx * OTBjMjg1NTQ2OTQ2ZmNiZTFhN2FjMTlkY2FhOWMxZTU1ZTM0MGQxOGNhMTJmN2FlNDI3Iiwic291cmNlX3NoYTI1Nl9yb2xlIjoi * cmV2aWV3ZWQgcGFyZW50LCBub3QgYSBzZWxmLXJlZmVyZW50aWFsIGNhbmRpZGF0ZSBoYXNoIiwicmV2aXNpb24iOiJSMDUgcmV2 * aWV3ZWQgUjA0IHJldHVybjsgYm91bmRlZCByZXBhaXJzLCBpc29sYXRlZCBldmlkZW5jZSwgcHJlc2VydmVkIGFyY2hpdGVjdHVy * ZS9VSSJ9 * NSU-RECOVERY-V1-END */