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"]); +});