diff options
| author | Adam Soltys <[email protected]> | 2026-08-30 21:47:36 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-30 21:47:36 -0700 |
| commit | 31748c85b02e725e75f3b4ebbef7c0cf17f1ecac (patch) | |
| tree | 740a0531729404701c1da423358fcb14110e2b76 /names | |
| parent | 3acc52af43148b7743fcd00361bcb8d050207651 (diff) | |
| download | coinosv3-31748c85b02e725e75f3b4ebbef7c0cf17f1ecac.tar.gz coinosv3-31748c85b02e725e75f3b4ebbef7c0cf17f1ecac.zip | |
The registrar learns who a migrated name belongs to: coinos.io's migrate action — running with the old account's authenticated session, which IS the ownership proof — posts a migrate-grant (Bearer migrateToken) naming the v3 destination, and the claim guard honors that grant where the identity-only check (old account's npub must sign the claim) could never pass for a v3 wallet key; without it every migrated claim 409'd forever and the wizard spun on 'Claiming…'
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'names')
| -rw-r--r-- | names/server.js | 39 |
1 files changed, 38 insertions, 1 deletions
diff --git a/names/server.js b/names/server.js index 1cb1d98..c0b3c1f 100644 --- a/names/server.js +++ b/names/server.js @@ -1020,6 +1020,37 @@ Bun.serve({ // Top up the float over Lightning: it mints an invoice against its own // ark balance, which anyone (in practice, the operator's node) can pay. // Guarded by a token from the config file — this endpoint is public. + // Legacy migration hand-off. coinos.io's migrate action (running with + // the OLD account's authenticated session — that session IS the proof of + // ownership) tells us which v3 identity may claim the released name. + // Without this the claim guard has nothing to go on: identity-only proof + // (the old account's npub signing the claim) never matches a v3 wallet's + // key, so every migrated claim 409'd forever. The grant names either the + // destination address (we look up its record) or a pubkey directly (the + // operator-rescue form). + if (url.pathname === '/migrate-grant' && req.method === 'POST') { + const tok = (req.headers.get('authorization') || '').replace(/^Bearer\s+/i, ''); + if (!CFG.migrateToken || tok !== CFG.migrateToken) return json({ error: 'unauthorized' }, 401); + let b; + try { b = await req.json(); } catch { return json({ error: 'bad body' }, 400); } + const name = String(b.name || '').toLowerCase(); + if (!NAME_RE.test(name)) return json({ error: 'bad name' }, 400); + let pubkey = /^[0-9a-f]{64}$/.test(String(b.pubkey || '')) ? b.pubkey : null; + let manager = null; + if (!pubkey && b.to) { + const [tn, td] = String(b.to).toLowerCase().split('@'); + const rec = state.names[`${tn}@${td}`]; + if (!rec) return json({ error: 'destination is not registered here' }, 404); + pubkey = rec.pubkey; + manager = rec.manager || null; + } + if (!pubkey) return json({ error: 'no destination' }, 400); + (state.migrations ||= {})[`${name}@coinos.io`] = { pubkey, manager, ts: Date.now() }; + persist(); + log(`migrate-grant: ${name}@coinos.io -> ${pubkey.slice(0, 8)}`); + return json({ ok: true }); + } + if (url.pathname === '/admin/topup' && req.method === 'POST') { if (!CFG.adminToken || req.headers.get('x-admin-token') !== CFG.adminToken) { return json({ error: 'unauthorized' }, 401); @@ -1106,7 +1137,13 @@ Bun.serve({ if (existing && existing.pubkey !== auth.pubkey && existing.manager !== auth.pubkey) { return json({ error: 'name is taken' }, 409); } - if (!existing && await takenByCoinosUser(domain, name, auth.pubkey)) return json({ error: 'name is taken' }, 409); + // A migration grant (the legacy account's authenticated session told + // us where the name is going) trumps the legacy reservation — the + // identity check below it can never pass for a v3 wallet key. + const grant = (state.migrations || {})[`${name}@coinos.io`]; + const granted = domain === 'coinos.io' && grant + && (grant.pubkey === auth.pubkey || (grant.manager && grant.manager === auth.pubkey)); + if (!existing && !granted && await takenByCoinosUser(domain, name, auth.pubkey)) return json({ error: 'name is taken' }, 409); // an npub-shaped name belongs to that identity alone if (npubPrefixOwner(name)) { const managerOk = existing && existing.manager === auth.pubkey; |
