aboutsummaryrefslogtreecommitdiffstats
path: root/names
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-30 21:47:36 -0700
committerAdam Soltys <[email protected]>2026-08-30 21:47:36 -0700
commit31748c85b02e725e75f3b4ebbef7c0cf17f1ecac (patch)
tree740a0531729404701c1da423358fcb14110e2b76 /names
parent3acc52af43148b7743fcd00361bcb8d050207651 (diff)
downloadcoinosv3-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.js39
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;