From 426296d293109699e2dcacef087689c4e60c47f2 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 22:57:29 +0000 Subject: [PATCH] chore(moshpit): revoke the 622 endings that collide with real TLDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.sh` was sold through the ordinary registration flow, for $2, to someone unrelated to this project. It is Saint Helena's ccTLD and it is also where this registry lives: pit.moshcode.sh. Every bridge that loaded the ending list then classified the registry as a Moshpit name, refused to forward it, and tried to resolve it through the registry it had just made unreachable — so machines running a bridge answered NXDOMAIN to the entire internet, and took the rest of the real .sh ccTLD down with them. v0.80.0 closed both halves going forward: the registry stopped selling endings IANA delegates, and bridges stopped answering for the ones already sold. This removes those from the directory. 622 endings, across 14 owners, at the time of writing. Archived rather than dropped. Selling an ending took money and the directory row is the only record of who holds what, so the rows move to `moshpit_tlds_removed` with their owner, price and original claim time intact — readable, refundable against, restorable. `moshpit_tld_log` is the append-only record this schema already keeps, and gains a `revoke` entry per ending, so the history reads as a claim that was revoked rather than one that silently stopped existing. Nothing cascades. moshpit_offers, moshpit_name_purchases and the rest store the ending as plain text with no foreign key, so an offer or a purchase against a revoked ending survives and stays auditable. Tested rather than reviewed. The migration is executed in apps/pwa/test/moshpit-revoke-real-tlds.test.mjs against the real schema and the real 1438-entry list, splitting statements exactly as migrate.mjs does. The failure worth guarding against is not that it errors — it is that it removes one row too many, which on this table means taking someone's ending away and which nothing downstream would notice. So `.hacker`, `.2600`, `.eggs`, `.moshpit` and `.42` are asserted present afterwards, punycode (`xn--p1ai`) is asserted removed, the archive is asserted to carry owner and price, and running it twice is asserted to remove nothing further. Suite: 2754 tests, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ThnQwoieWt8VR6N7gtgnhp --- .../019_moshpit_revoke_real_tlds.sql | 65 ++++++++++ .../test/moshpit-revoke-real-tlds.test.mjs | 117 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 apps/pwa/src/migrations/019_moshpit_revoke_real_tlds.sql create mode 100644 apps/pwa/test/moshpit-revoke-real-tlds.test.mjs diff --git a/apps/pwa/src/migrations/019_moshpit_revoke_real_tlds.sql b/apps/pwa/src/migrations/019_moshpit_revoke_real_tlds.sql new file mode 100644 index 0000000..2fcd854 --- /dev/null +++ b/apps/pwa/src/migrations/019_moshpit_revoke_real_tlds.sql @@ -0,0 +1,65 @@ +-- Remove the endings that collide with real top-level domains. +-- +-- `.sh` was sold, through the ordinary registration flow, for $2. It is Saint +-- Helena's ccTLD and it is also where this registry lives: pit.moshcode.sh. +-- Every Moshpit bridge that loaded the ending list then treated the registry as +-- a Moshpit name, refused to forward it, and tried to resolve it through the +-- registry it had just made unreachable. Machines running a bridge answered +-- NXDOMAIN to the entire internet, and the rest of the real .sh ccTLD with it. +-- +-- v0.80.0 stopped new ones being sold and stopped bridges answering for the +-- ones already sold. This removes those from the directory: 622 endings across +-- 14 owners at the time of writing. +-- +-- Archived, not dropped. Selling an ending took money and the rows are the only +-- record of who holds what, so they move to `moshpit_tlds_removed` where they +-- can be read, refunded against, or put back. `moshpit_tld_log` is the +-- append-only record this schema already keeps, and it gains a `revoke` entry +-- per ending, so the history reads correctly rather than showing a claim that +-- silently stopped existing. +-- +-- Nothing cascades: moshpit_offers, moshpit_name_purchases and the rest store +-- the ending as plain text with no foreign key, so an offer or a purchase +-- against a revoked ending survives this and stays auditable. + +CREATE TABLE IF NOT EXISTS moshpit_tlds_removed ( + tld TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + owner_email TEXT, + owner_key TEXT, + alias_of TEXT, + price_usd REAL, + created_at INTEGER NOT NULL, + removed_at INTEGER NOT NULL, + reason TEXT NOT NULL +); + +-- Scratch, dropped at the end. Held as a table rather than a 1438-value IN () +-- repeated three times, which would be most of this file. +CREATE TABLE IF NOT EXISTS _iana_real_tlds (tld TEXT PRIMARY KEY); + +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('aaa'),('aarp'),('abb'),('abbott'),('abbvie'),('abc'),('able'),('abogado'),('abudhabi'),('ac'),('academy'),('accenture'),('accountant'),('accountants'),('aco'),('actor'),('ad'),('ads'),('adult'),('ae'),('aeg'),('aero'),('aetna'),('af'),('afl'),('africa'),('ag'),('agakhan'),('agency'),('ai'),('aig'),('airbus'),('airforce'),('airtel'),('akdn'),('al'),('alibaba'),('alipay'),('allfinanz'),('allstate'),('ally'),('alsace'),('alstom'),('am'),('amazon'),('americanexpress'),('americanfamily'),('amex'),('amfam'),('amica'),('amsterdam'),('analytics'),('android'),('anquan'),('anz'),('ao'),('aol'),('apartments'),('app'),('apple'),('aq'),('aquarelle'),('ar'),('arab'),('aramco'),('archi'),('army'),('arpa'),('art'),('arte'),('as'),('asda'),('asia'),('associates'),('at'),('athleta'),('attorney'),('au'),('auction'),('audi'),('audible'),('audio'),('auspost'),('author'),('auto'),('autos'),('aw'),('aws'),('ax'),('axa'),('az'),('azure'),('ba'),('baby'),('baidu'),('banamex'),('band'),('bank'),('bar'),('barcelona'),('barclaycard'),('barclays'),('barefoot'),('bargains'),('baseball'),('basketball'),('bauhaus'),('bayern'),('bb'),('bbc'),('bbt'),('bbva'),('bcg'),('bcn'),('bd'),('be'),('beats'),('beauty'),('beer'),('berlin'),('best'),('bestbuy'),('bet'),('bf'),('bg'),('bh'),('bharti'),('bi'),('bible'),('bid'),('bike'),('bing'),('bingo'),('bio'),('biz'),('bj'),('black'),('blackfriday'),('blockbuster'),('blog'),('bloomberg'),('blue'),('bm'),('bms'),('bmw'),('bn'),('bnpparibas'),('bo'),('boats'),('boehringer'),('bofa'),('bom'),('bond'),('boo'),('book'),('booking'),('bosch'),('bostik'),('boston'),('bot'),('boutique'),('box'),('br'),('bradesco'),('bridgestone'),('broadway'),('broker'),('brother'),('brussels'),('bs'),('bt'),('build'),('builders'),('business'),('buy'),('buzz'),('bv'),('bw'),('by'),('bz'),('bzh'),('ca'),('cab'),('cafe'),('cal'),('call'),('calvinklein'),('cam'),('camera'),('camp'),('canon'),('capetown'),('capital'),('capitalone'),('car'),('caravan'),('cards'),('care'),('career'),('careers'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('cars'),('casa'),('case'),('cash'),('casino'),('cat'),('catering'),('catholic'),('cba'),('cbn'),('cbre'),('cc'),('cd'),('center'),('ceo'),('cern'),('cf'),('cfa'),('cfd'),('cg'),('ch'),('chanel'),('channel'),('charity'),('chase'),('chat'),('cheap'),('chintai'),('christmas'),('chrome'),('church'),('ci'),('cipriani'),('circle'),('cisco'),('citadel'),('citi'),('citic'),('city'),('ck'),('cl'),('claims'),('cleaning'),('click'),('clinic'),('clinique'),('clothing'),('cloud'),('club'),('clubmed'),('cm'),('cn'),('co'),('coach'),('codes'),('coffee'),('college'),('cologne'),('com'),('commbank'),('community'),('company'),('compare'),('computer'),('comsec'),('condos'),('construction'),('consulting'),('contact'),('contractors'),('cooking'),('cool'),('coop'),('corsica'),('country'),('coupon'),('coupons'),('courses'),('cpa'),('cr'),('credit'),('creditcard'),('creditunion'),('cricket'),('crown'),('crs'),('cruise'),('cruises'),('cu'),('cuisinella'),('cv'),('cw'),('cx'),('cy'),('cymru'),('cyou'),('cz'),('dad'),('dance'),('data'),('date'),('dating'),('datsun'),('day'),('dclk'),('dds'),('de'),('deal'),('dealer'),('deals'),('degree'),('delivery'),('dell'),('deloitte'),('delta'),('democrat'),('dental'),('dentist'),('desi'),('design'),('dev'),('dhl'),('diamonds'),('diet'),('digital'),('direct'),('directory'),('discount'),('discover'),('dish'),('diy'),('dj'),('dk'),('dm'),('dnp'),('do'),('docs'),('doctor'),('dog'),('domains'),('dot'),('download'),('drive'),('dtv'),('dubai'),('dupont'),('durban'),('dvag'),('dvr'),('dz'),('earth'),('eat'),('ec'),('eco'),('edeka'),('edu'),('education'),('ee'),('eg'),('email'),('emerck'),('energy'),('engineer'),('engineering'),('enterprises'),('epson'),('equipment'),('er'),('ericsson'),('erni'),('es'),('esq'),('estate'),('et'),('eu'),('eurovision'),('eus'),('events'),('exchange'),('expert'),('exposed'),('express'),('extraspace'),('fage'),('fail'),('fairwinds'),('faith'),('family'),('fan'),('fans'),('farm'),('farmers'),('fashion'),('fast'),('fedex'),('feedback'),('ferrari'),('ferrero'),('fi'),('fidelity'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('fido'),('film'),('final'),('finance'),('financial'),('fire'),('firestone'),('firmdale'),('fish'),('fishing'),('fit'),('fitness'),('fj'),('fk'),('flickr'),('flights'),('flir'),('florist'),('flowers'),('fly'),('fm'),('fo'),('foo'),('food'),('football'),('ford'),('forex'),('forsale'),('forum'),('foundation'),('fox'),('fr'),('free'),('fresenius'),('frl'),('frogans'),('frontier'),('ftr'),('fujitsu'),('fun'),('fund'),('furniture'),('futbol'),('fyi'),('ga'),('gal'),('gallery'),('gallo'),('gallup'),('game'),('games'),('gap'),('garden'),('gay'),('gb'),('gbiz'),('gd'),('gdn'),('ge'),('gea'),('gent'),('genting'),('george'),('gf'),('gg'),('ggee'),('gh'),('gi'),('gift'),('gifts'),('gives'),('giving'),('gl'),('glass'),('gle'),('global'),('globo'),('gm'),('gmail'),('gmbh'),('gmo'),('gmx'),('gn'),('godaddy'),('gold'),('goldpoint'),('golf'),('goodyear'),('goog'),('google'),('gop'),('got'),('gov'),('gp'),('gq'),('gr'),('grainger'),('graphics'),('gratis'),('green'),('gripe'),('grocery'),('group'),('gs'),('gt'),('gu'),('gucci'),('guge'),('guide'),('guitars'),('guru'),('gw'),('gy'),('hair'),('hamburg'),('hangout'),('haus'),('hbo'),('hdfc'),('hdfcbank'),('health'),('healthcare'),('help'),('helsinki'),('here'),('hermes'),('hiphop'),('hisamitsu'),('hitachi'),('hiv'),('hk'),('hkt'),('hm'),('hn'),('hockey'),('holdings'),('holiday'),('homedepot'),('homegoods'),('homes'),('homesense'),('honda'),('horse'),('hospital'),('host'),('hosting'),('hot'),('hotels'),('hotmail'),('house'),('how'),('hr'),('hsbc'),('ht'),('hu'),('hughes'),('hyatt'),('hyundai'),('ibm'),('icbc'),('ice'),('icu'),('id'),('ie'),('ieee'),('ifm'),('ikano'),('il'),('im'),('imamat'),('imdb'),('immo'),('immobilien'),('in'),('inc'),('industries'),('infiniti'),('info'),('ing'),('ink'),('institute'),('insurance'),('insure'),('int'),('international'),('intuit'),('investments'),('io'),('ipiranga'),('iq'),('ir'),('irish'),('is'),('ismaili'),('ist'),('istanbul'),('it'),('itau'),('itv'),('jaguar'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('java'),('jcb'),('je'),('jeep'),('jetzt'),('jewelry'),('jio'),('jll'),('jm'),('jmp'),('jnj'),('jo'),('jobs'),('joburg'),('jot'),('joy'),('jp'),('jpmorgan'),('jprs'),('juegos'),('juniper'),('kaufen'),('kddi'),('ke'),('kerryhotels'),('kerryproperties'),('kfh'),('kg'),('kh'),('ki'),('kia'),('kids'),('kim'),('kindle'),('kitchen'),('kiwi'),('km'),('kn'),('koeln'),('komatsu'),('kosher'),('kp'),('kpmg'),('kpn'),('kr'),('krd'),('kred'),('kuokgroup'),('kw'),('ky'),('kyoto'),('kz'),('la'),('lacaixa'),('lamborghini'),('lamer'),('land'),('landrover'),('lanxess'),('lasalle'),('lat'),('latino'),('latrobe'),('law'),('lawyer'),('lb'),('lc'),('lds'),('lease'),('leclerc'),('lefrak'),('legal'),('lego'),('lexus'),('lgbt'),('li'),('lidl'),('life'),('lifeinsurance'),('lifestyle'),('lighting'),('like'),('lilly'),('limited'),('limo'),('lincoln'),('link'),('live'),('living'),('lk'),('llc'),('llp'),('loan'),('loans'),('locker'),('locus'),('lol'),('london'),('lotte'),('lotto'),('love'),('lpl'),('lplfinancial'),('lr'),('ls'),('lt'),('ltd'),('ltda'),('lu'),('lundbeck'),('luxe'),('luxury'),('lv'),('ly'),('ma'),('madrid'),('maif'),('maison'),('makeup'),('man'),('management'),('mango'),('map'),('market'),('marketing'),('markets'),('marriott'),('marshalls'),('mattel'),('mba'),('mc'),('mckinsey'),('md'),('me'),('med'),('media'),('meet'),('melbourne'),('meme'),('memorial'),('men'),('menu'),('merck'),('merckmsd'),('mg'),('mh'),('miami'),('microsoft'),('mil'),('mini'),('mint'),('mit'),('mitsubishi'),('mk'),('ml'),('mlb'),('mls'),('mm'),('mma'),('mn'),('mo'),('mobi'),('mobile'),('moda'),('moe'),('moi'),('mom'),('monash'),('money'),('monster'),('mormon'),('mortgage'),('moscow'),('moto'),('motorcycles'),('mov'),('movie'),('mp'),('mq'),('mr'),('ms'),('msd'),('mt'),('mtn'),('mtr'),('mu'),('museum'),('music'),('mv'),('mw'),('mx'),('my'),('mz'),('na'),('nab'),('nagoya'),('name'),('navy'),('nba'),('nc'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('ne'),('nec'),('net'),('netbank'),('netflix'),('network'),('neustar'),('new'),('news'),('next'),('nextdirect'),('nexus'),('nf'),('nfl'),('ng'),('ngo'),('nhk'),('ni'),('nico'),('nike'),('nikon'),('ninja'),('nissan'),('nissay'),('nl'),('no'),('nokia'),('norton'),('now'),('nowruz'),('nowtv'),('np'),('nr'),('nra'),('nrw'),('ntt'),('nu'),('nyc'),('nz'),('obi'),('observer'),('office'),('okinawa'),('olayan'),('olayangroup'),('ollo'),('om'),('omega'),('one'),('ong'),('onl'),('online'),('ooo'),('open'),('oracle'),('orange'),('org'),('organic'),('origins'),('osaka'),('otsuka'),('ott'),('ovh'),('pa'),('page'),('panasonic'),('paris'),('pars'),('partners'),('parts'),('party'),('pay'),('pccw'),('pe'),('pet'),('pf'),('pfizer'),('pg'),('ph'),('pharmacy'),('phd'),('philips'),('phone'),('photo'),('photography'),('photos'),('physio'),('pics'),('pictet'),('pictures'),('pid'),('pin'),('ping'),('pink'),('pioneer'),('pizza'),('pk'),('pl'),('place'),('play'),('playstation'),('plumbing'),('plus'),('pm'),('pn'),('pnc'),('pohl'),('poker'),('politie'),('porn'),('post'),('pr'),('praxi'),('press'),('prime'),('pro'),('prod'),('productions'),('prof'),('progressive'),('promo'),('properties'),('property'),('protection'),('pru'),('prudential'),('ps'),('pt'),('pub'),('pw'),('pwc'),('py'),('qa'),('qpon'),('quebec'),('quest'),('racing'),('radio'),('re'),('read'),('realestate'),('realtor'),('realty'),('recipes'),('red'),('redumbrella'),('rehab'),('reise'),('reisen'),('reit'),('reliance'),('ren'),('rent'),('rentals'),('repair'),('report'),('republican'),('rest'),('restaurant'),('review'),('reviews'),('rexroth'),('rich'),('richardli'),('ricoh'),('ril'),('rio'),('rip'),('ro'),('rocks'),('rodeo'),('rogers'),('room'),('rs'),('rsvp'),('ru'),('rugby'),('ruhr'),('run'),('rw'),('rwe'),('ryukyu'),('sa'),('saarland'),('safe'),('safety'),('sakura'),('sale'),('salon'),('samsclub'),('samsung'),('sandvik'),('sandvikcoromant'),('sanofi'),('sap'),('sarl'),('sas'),('save'),('saxo'),('sb'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('sbi'),('sbs'),('sc'),('scb'),('schaeffler'),('schmidt'),('scholarships'),('school'),('schule'),('schwarz'),('science'),('scot'),('sd'),('se'),('search'),('seat'),('secure'),('security'),('seek'),('select'),('sener'),('services'),('seven'),('sew'),('sex'),('sexy'),('sfr'),('sg'),('sh'),('shangrila'),('sharp'),('shell'),('shia'),('shiksha'),('shoes'),('shop'),('shopping'),('shouji'),('show'),('si'),('silk'),('sina'),('singles'),('site'),('sj'),('sk'),('ski'),('skin'),('sky'),('skype'),('sl'),('sling'),('sm'),('smart'),('smile'),('sn'),('sncf'),('so'),('soccer'),('social'),('softbank'),('software'),('sohu'),('solar'),('solutions'),('song'),('sony'),('soy'),('spa'),('space'),('sport'),('spot'),('sr'),('srl'),('ss'),('st'),('stada'),('staples'),('star'),('statebank'),('statefarm'),('stc'),('stcgroup'),('stockholm'),('storage'),('store'),('stream'),('studio'),('study'),('style'),('su'),('sucks'),('supplies'),('supply'),('support'),('surf'),('surgery'),('suzuki'),('sv'),('swatch'),('swiss'),('sx'),('sy'),('sydney'),('systems'),('sz'),('tab'),('taipei'),('talk'),('taobao'),('target'),('tatamotors'),('tatar'),('tattoo'),('tax'),('taxi'),('tc'),('tci'),('td'),('tdk'),('team'),('tech'),('technology'),('tel'),('temasek'),('tennis'),('teva'),('tf'),('tg'),('th'),('thd'),('theater'),('theatre'),('tiaa'),('tickets'),('tienda'),('tips'),('tires'),('tirol'),('tj'),('tjmaxx'),('tjx'),('tk'),('tkmaxx'),('tl'),('tm'),('tmall'),('tn'),('to'),('today'),('tokyo'),('tools'),('top'),('toray'),('toshiba'),('total'),('tours'),('town'),('toyota'),('toys'),('tr'),('trade'),('trading'),('training'),('travel'),('travelers'),('travelersinsurance'),('trust'),('trv'),('tt'),('tube'),('tui'),('tunes'),('tushu'),('tv'),('tvs'),('tw'),('tz'),('ua'),('ubank'),('ubs'),('ug'),('uk'),('unicom'),('university'),('uno'),('uol'),('ups'),('us'),('uy'),('uz'),('va'),('vacations'),('vana'),('vanguard'),('vc'),('ve'),('vegas'),('ventures'),('verisign'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('versicherung'),('vet'),('vg'),('vi'),('viajes'),('video'),('vig'),('viking'),('villas'),('vin'),('vip'),('virgin'),('visa'),('vision'),('viva'),('vivo'),('vlaanderen'),('vn'),('vodka'),('volvo'),('vote'),('voting'),('voto'),('voyage'),('vu'),('wales'),('walmart'),('walter'),('wang'),('wanggou'),('watch'),('watches'),('weather'),('weatherchannel'),('web'),('webcam'),('weber'),('website'),('wed'),('wedding'),('weibo'),('weir'),('wf'),('whoswho'),('wien'),('wiki'),('williamhill'),('win'),('windows'),('wine'),('winners'),('wme'),('woodside'),('work'),('works'),('world'),('wow'),('ws'),('wtc'),('wtf'),('xbox'),('xerox'),('xihuan'),('xin'),('xn--11b4c3d'),('xn--1ck2e1b'),('xn--1qqw23a'),('xn--2scrj9c'),('xn--30rr7y'),('xn--3bst00m'),('xn--3ds443g'),('xn--3e0b707e'),('xn--3hcrj9c'),('xn--3pxu8k'),('xn--42c2d9a'),('xn--45br5cyl'),('xn--45brj9c'),('xn--45q11c'),('xn--4dbrk0ce'),('xn--4gbrim'),('xn--54b7fta0cc'),('xn--55qw42g'),('xn--55qx5d'),('xn--5su34j936bgsg'),('xn--5tzm5g'),('xn--6frz82g'),('xn--6qq986b3xl'),('xn--80adxhks'),('xn--80ao21a'),('xn--80aqecdr1a'),('xn--80asehdb'),('xn--80aswg'),('xn--8y0a063a'),('xn--90a3ac'),('xn--90ae'),('xn--90ais'),('xn--9dbq2a'),('xn--9et52u'),('xn--9krt00a'),('xn--b4w605ferd'),('xn--bck1b9a5dre4c'),('xn--c1avg'),('xn--c2br7g'),('xn--cck2b3b'),('xn--cckwcxetd'),('xn--cg4bki'),('xn--clchc0ea0b2g2a9gcd'),('xn--czr694b'),('xn--czrs0t'),('xn--czru2d'),('xn--d1acj3b'),('xn--d1alf'),('xn--e1a4c'),('xn--eckvdtc9d'),('xn--efvy88h'),('xn--fct429k'),('xn--fhbei'),('xn--fiq228c5hs'),('xn--fiq64b'),('xn--fiqs8s'),('xn--fiqz9s'),('xn--fjq720a'),('xn--flw351e'),('xn--fpcrj9c3d'),('xn--fzc2c9e2c'),('xn--fzys8d69uvgm'),('xn--g2xx48c'),('xn--gckr3f0f'),('xn--gecrj9c'),('xn--gk3at1e'),('xn--h2breg3eve'),('xn--h2brj9c'),('xn--h2brj9c8c'),('xn--hxt814e'),('xn--i1b6b1a6a2e'),('xn--imr513n'),('xn--io0a7i'),('xn--j1aef'),('xn--j1amh'),('xn--j6w193g'),('xn--jlq480n2rg'),('xn--jvr189m'),('xn--kcrx77d1x4a'),('xn--kprw13d'),('xn--kpry57d'),('xn--kput3i'),('xn--l1acc'),('xn--lgbbat1ad8j'),('xn--mgb9awbf'),('xn--mgba3a3ejt'),('xn--mgba3a4f16a'),('xn--mgba7c0bbn0a'),('xn--mgbaam7a8h'),('xn--mgbab2bd'),('xn--mgbah1a3hjkrd'),('xn--mgbai9azgqp6j'),('xn--mgbayh7gpa'),('xn--mgbbh1a'),('xn--mgbbh1a71e'),('xn--mgbc0a9azcg'),('xn--mgbca7dzdo'),('xn--mgbcpq6gpa1a'),('xn--mgberp4a5d4ar'),('xn--mgbgu82a'),('xn--mgbi4ecexp'),('xn--mgbpl2fh'),('xn--mgbt3dhd'),('xn--mgbtx2b'),('xn--mgbx4cd0ab'),('xn--mix891f'),('xn--mk1bu44c'),('xn--mxtq1m'),('xn--ngbc5azd'),('xn--ngbe9e0a'),('xn--ngbrx'),('xn--node'),('xn--nqv7f'),('xn--nqv7fs00ema'),('xn--nyqy26a'),('xn--o3cw4h'),('xn--ogbpf8fl'),('xn--otu796d'),('xn--p1acf'),('xn--p1ai'),('xn--pgbs0dh'),('xn--pssy2u'),('xn--q7ce6a'),('xn--q9jyb4c'),('xn--qcka1pmc'),('xn--qxa6a'),('xn--qxam'),('xn--rhqv96g'),('xn--rovu88b'),('xn--rvc1e0am3e'),('xn--s9brj9c'),('xn--ses554g'),('xn--t60b56a'),('xn--tckwe'),('xn--tiq49xqyj'),('xn--unup4y'); +INSERT OR IGNORE INTO _iana_real_tlds (tld) VALUES ('xn--vermgensberater-ctb'),('xn--vermgensberatung-pwb'),('xn--vhquv'),('xn--vuq861b'),('xn--w4r85el8fhu5dnra'),('xn--w4rs40l'),('xn--wgbh1c'),('xn--wgbl6a'),('xn--xhq521b'),('xn--xkc2al3hye2a'),('xn--xkc2dl3a5ee0h'),('xn--y9a3aq'),('xn--yfro4i67o'),('xn--ygbi2ammx'),('xn--zfr164b'),('xxx'),('xyz'),('yachts'),('yahoo'),('yamaxun'),('yandex'),('ye'),('yodobashi'),('yoga'),('yokohama'),('you'),('youtube'),('yt'),('yun'),('za'),('zappos'),('zara'),('zero'),('zip'),('zm'),('zone'),('zuerich'),('zw'); + +INSERT OR IGNORE INTO moshpit_tlds_removed + (tld, user_id, owner_email, owner_key, alias_of, price_usd, created_at, removed_at, reason) +SELECT t.tld, t.user_id, t.owner_email, t.owner_key, t.alias_of, t.price_usd, t.created_at, + CAST(strftime('%s','now') AS INTEGER) * 1000, + 'collides with a real IANA top-level domain' +FROM moshpit_tlds t +JOIN _iana_real_tlds r ON r.tld = t.tld; + +INSERT INTO moshpit_tld_log (tld, user_id, action, at) +SELECT t.tld, t.user_id, 'revoke', CAST(strftime('%s','now') AS INTEGER) * 1000 +FROM moshpit_tlds t +JOIN _iana_real_tlds r ON r.tld = t.tld; + +DELETE FROM moshpit_tlds WHERE tld IN (SELECT tld FROM _iana_real_tlds); + +DROP TABLE _iana_real_tlds; diff --git a/apps/pwa/test/moshpit-revoke-real-tlds.test.mjs b/apps/pwa/test/moshpit-revoke-real-tlds.test.mjs new file mode 100644 index 0000000..f6aaa08 --- /dev/null +++ b/apps/pwa/test/moshpit-revoke-real-tlds.test.mjs @@ -0,0 +1,117 @@ +// The migration that removes endings colliding with real top-level domains. +// +// Run once, against production, deleting rows people paid for. That is not a +// thing to merge on the strength of having read it, so it is executed here +// against a fixture with the real schema and the real 1438-entry list, and the +// outcome asserted. +// +// The failure being guarded against is not "it errors". It is "it removes one +// row too many", which on this table means taking someone's ending away, and +// which nothing downstream would notice. +import test from "node:test"; +import assert from "node:assert/strict"; +import { DatabaseSync } from "node:sqlite"; +import { readFile } from "node:fs/promises"; + +const MIGRATION = new URL("../src/migrations/019_moshpit_revoke_real_tlds.sql", import.meta.url); + +/** The runner splits on `;` at end of line; this must match it exactly. */ +const statements = (sql) => sql.split(/;\s*(?:\n|$)/).map((s) => s.trim()).filter(Boolean); + +function fixture() { + const db = new DatabaseSync(":memory:"); + db.exec(` + CREATE TABLE users (id TEXT PRIMARY KEY); + CREATE TABLE moshpit_tlds ( + tld TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + owner_email TEXT, owner_key TEXT, alias_of TEXT, created_at INTEGER NOT NULL, price_usd REAL); + CREATE TABLE moshpit_tld_log ( + seq INTEGER PRIMARY KEY AUTOINCREMENT, tld TEXT NOT NULL, user_id TEXT NOT NULL, + action TEXT NOT NULL, at INTEGER NOT NULL); + CREATE TABLE moshpit_offers (id TEXT PRIMARY KEY, tld TEXT NOT NULL); + INSERT INTO users (id) VALUES ('u1'),('u2'); + `); + const rows = [ + // Real top-level domains. `sh` is the one that started this: sold for $2, + // and it is where the registry itself lives. + ["sh", "u2", 2], ["ai", "u1", 500], ["dev", "u2", null], ["com", "u1", 1], + // Punycode is how an IDN appears on the wire, so it has to match too. + ["xn--p1ai", "u1", null], + // Moshpit's own. These must survive untouched. + ["hacker", "u1", null], ["2600", "u1", 2], ["eggs", "u1", null], + ["moshpit", "u1", null], ["42", "u2", null], + ]; + const insert = db.prepare( + `INSERT INTO moshpit_tlds (tld,user_id,owner_email,owner_key,alias_of,created_at,price_usd) + VALUES (?,?,?,?,?,?,?)`, + ); + for (const [tld, user, price] of rows) insert.run(tld, user, `${user}@x.test`, null, null, 1700000000000, price); + db.exec(`INSERT INTO moshpit_offers (id,tld) VALUES ('o1','sh')`); + return db; +} + +async function applied() { + const db = fixture(); + for (const stmt of statements(await readFile(MIGRATION, "utf8"))) db.exec(stmt); + return db; +} + +const column = (db, sql) => db.prepare(sql).all().map((r) => Object.values(r)[0]).sort(); + +test("every real top-level domain is removed, punycode included", async () => { + const db = await applied(); + assert.deepEqual(column(db, "SELECT tld FROM moshpit_tlds_removed"), ["ai", "com", "dev", "sh", "xn--p1ai"]); +}); + +test("Moshpit's own endings are untouched", async () => { + const db = await applied(); + // The whole risk of this migration in one assertion. + assert.deepEqual(column(db, "SELECT tld FROM moshpit_tlds"), ["2600", "42", "eggs", "hacker", "moshpit"]); +}); + +test("rows are archived, not dropped — owner and price survive", async () => { + // Selling an ending took money. The directory row is the only record of who + // holds what, so a hard delete would destroy the thing a refund would be + // computed from. + const db = await applied(); + const row = db.prepare("SELECT * FROM moshpit_tlds_removed WHERE tld = 'sh'").get(); + assert.equal(row.user_id, "u2"); + assert.equal(row.owner_email, "u2@x.test"); + assert.equal(row.price_usd, 2); + assert.equal(row.created_at, 1700000000000); + assert.match(row.reason, /real IANA top-level domain/); + assert.ok(row.removed_at > 1700000000000, "removed_at is set to now, not copied"); +}); + +test("the append-only log gains a revocation for each one", async () => { + // `moshpit_tld_log` is the record; the directory is a cache of it. A claim + // that stopped existing with nothing logged would read as never having been + // made. + const db = await applied(); + assert.deepEqual( + column(db, "SELECT tld FROM moshpit_tld_log WHERE action = 'revoke'"), + ["ai", "com", "dev", "sh", "xn--p1ai"], + ); +}); + +test("nothing cascades — an offer against a revoked ending survives", async () => { + const db = await applied(); + assert.equal(db.prepare("SELECT COUNT(*) AS n FROM moshpit_offers").get().n, 1); +}); + +test("the scratch table is cleaned up", async () => { + const db = await applied(); + const n = db.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE name = '_iana_real_tlds'").get().n; + assert.equal(n, 0, "a leftover table would be re-used, and stale, on the next migration"); +}); + +test("running it twice removes nothing further", async () => { + // The runner tracks applied files, so this should never happen — but a + // migration that is not idempotent is one bad `_migrations` row away from + // deleting a second set of endings. + const db = await applied(); + const sql = await readFile(MIGRATION, "utf8"); + for (const stmt of statements(sql)) db.exec(stmt); + assert.deepEqual(column(db, "SELECT tld FROM moshpit_tlds"), ["2600", "42", "eggs", "hacker", "moshpit"]); + assert.deepEqual(column(db, "SELECT tld FROM moshpit_tlds_removed"), ["ai", "com", "dev", "sh", "xn--p1ai"]); +});