diff options
| author | Adam Soltys <[email protected]> | 2026-10-26 08:35:54 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-26 08:35:54 -0700 |
| commit | ef1739e2de914f0a02990979f13057cef77c9a44 (patch) | |
| tree | 3aa55a4bf96a13ebf208b549996d549043e4eb9c | |
| parent | 85399e5f2a303155a9b437712aed08d62fbc8776 (diff) | |
| download | coinosv3-ef1739e2de914f0a02990979f13057cef77c9a44.tar.gz coinosv3-ef1739e2de914f0a02990979f13057cef77c9a44.zip | |
A released placeholder address on your profile page shows as the claimed one on the Lightning line too, and Save in the profile editor repoints it in the kind 0 (a same-domain name that no longer answers); the background heal steps aside for an interactive signer (Amber) instead of waiting forever in the kind-0 queue
Jarryd's page still showed [email protected] over ⚡ [email protected]: the header only substituted the NIP-05 line, and his kind 0 was never republished — the heal of ce96364 needs a background signature, which an Amber sign-in can't give, and Save left a lud16 that wasn't the old address alone.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
| -rw-r--r-- | src/features/messages.js | 23 | ||||
| -rw-r--r-- | tools/profile-publish-race-test.js | 18 |
2 files changed, 37 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 6f010b2..6c51d93 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12767,8 +12767,15 @@ export function messagesFeature(ctx) { // when empty; move them when they pointed at our (old) address. A // deliberately foreign lud16/nip05 is the user's business. if (addr) { - for (const k of ['lud16', 'nip05']) - if (!merged[k] || merged[k] === oldAddr) merged[k] = addr; + const domain = addr.split('@')[1]; + for (const k of ['lud16', 'nip05']) { + const cur = merged[k]; + if (!cur || cur === oldAddr) { merged[k] = addr; continue; } + // a name at our own domain that is not ours and no longer answers + // (the placeholder a migration released) is ours now — Save is the + // heal an interactive signer can't do in the background + if (typeof cur === 'string' && cur !== addr && cur.split('@')[1] === domain && !/^_@/.test(cur) && await addressDead(cur)) merged[k] = addr; + } } if (merged.name) merged.display_name = merged.name; const partial = { kind: 0, content: JSON.stringify(merged), tags: [], created_at: kind0Stamp(id.pubkey) }; @@ -12796,6 +12803,11 @@ export function messagesFeature(ctx) { // alone. Once per address per session. const addrHealed = new Set(); async function healStaleAddress(olds, myAddr) { + // Amber by intent needs a tap per signature: a background publish would + // sit in the kind-0 queue forever and block every later one. Save in the + // profile editor repoints the address for these users instead. + const id = await identity().catch(() => null); + if (!id || (id.signer && id.signer.interactive)) return; for (const old of olds) { if (addrHealed.has(old)) continue; addrHealed.add(old); @@ -12957,7 +12969,10 @@ export function messagesFeature(ctx) { if (full) { const olds = [...new Set([stale(full.lud16), stale(full.nip05)].filter(Boolean))]; if (olds.length) healStaleAddress(olds, myAddr); } const rawNip05 = (full && (stale(full.nip05) ? myAddr : full.nip05)) || (mine && myAddr) || lp.nip05 || lp.addr || null; const nip05 = rawNip05 ? String(rawNip05).replace(/^_@/, '') : null; - const lud16 = (full && full.lud16 ? String(full.lud16) : null) || lp.lud16 || null; + // the lightning line too: a stale same-domain lud16 (jar@ after the + // rename to jarryd@) showed under the right NIP-05 as a second address + const rawLud = (full && full.lud16 ? String(full.lud16) : null) || lp.lud16 || null; + const lud16 = rawLud ? (stale(rawLud) ? myAddr : rawLud) : null; const showLud = lud16 && !/^npub1/i.test(lud16) && lud16 !== nip05; // an about of "~" or a lone character is noise, not a bio. // Until the full kind 0 lands, the light profile cache answers — it was @@ -18404,6 +18419,8 @@ export function messagesFeature(ctx) { // lud16 and nip05 — but only where they pointed at the released address // (or were empty). Deliberately different values are not ours to touch. addressRenamed(oldAddr, newAddr) { return repointAddress(oldAddr, newAddr).catch(() => {}); }, + // the profile editor's Save, without the editor (tests) + async saveProfileDraft(e) { const id = await identity(); return withKind0Lock(() => saveProfileNow(id, { name: '', about: '', picture: '', banner: '', ...e })); }, // opens the profile, unless the caller has a step to take first (the // Accounts page switches to that identity before showing its profile) profileChip(pk, size, onOpen) { diff --git a/tools/profile-publish-race-test.js b/tools/profile-publish-race-test.js index 390c01b..becf375 100644 --- a/tools/profile-publish-race-test.js +++ b/tools/profile-publish-race-test.js @@ -67,9 +67,10 @@ const wallet = { loadFeatureState: () => null, saveFeatureState: () => {}, _cacheKey: () => 'race-test', saveCache: () => {}, registerCacheExtension: () => {}, registerLoadHook: () => {}, }; +let namesAddr = null; // what the registrar says our address is const feature = messagesFeature({ h: (tag, attrs, ...kids) => ({ tag, attrs, kids }), ui: {}, render: () => {}, wallet, toast: () => {}, brandHeader: () => null, - hook: (name) => (name === 'nostrLoginIdentity' ? { pubkey: pk, signer } : null), + hook: (name) => (name === 'nostrLoginIdentity' ? { pubkey: pk, signer } : name === 'namesAddress' ? namesAddr : null), }); console.log('[the migration: rename and avatar import fired together]'); @@ -91,5 +92,20 @@ await Promise.all([feature.addressRenamed('[email protected]', '[email protected]'), const last = JSON.parse(store.filter((e) => e.kind === 0 && e.pubkey === pk).sort((a, b) => b.created_at - a.created_at)[0].content); check('both changes survive', last.lud16 === '[email protected]' && last.about === 'hi' && /face/.test(last.picture), JSON.stringify(last)); +console.log('[Save in the editor repoints a dead same-domain address]'); +// The heal never ran (an Amber user: no background signatures), so the kind 0 +// still names the released placeholder. The domain says jar is dead, other +// is alive. +store.push(finalizeEvent({ kind: 0, created_at: now + 1000, tags: [], content: JSON.stringify({ name: 'Jarryd', lud16: '[email protected]', nip05: '[email protected]' }) }, sk)); +namesAddr = '[email protected]'; +globalThis.fetch = async (url) => /lnurlp\/jar$/.test(url) + ? { ok: false, status: 500, json: async () => null } + : { ok: true, status: 200, json: async () => ({ tag: 'payRequest', callback: 'https://x/cb' }) }; +published.length = 0; +await feature.saveProfileDraft({ uname: 'jarryd', name: 'Jarryd' }); +const saved = JSON.parse(published.filter((e) => e.kind === 0).pop().content); +check('the dead placeholder moves to the claimed address', saved.lud16 === '[email protected]', JSON.stringify(saved)); +check('a same-domain address that still answers is left alone', saved.nip05 === '[email protected]', saved.nip05); + console.log(fails ? `\n❌ ${fails} failing` : '\n✅ kind-0 publishes take turns'); process.exit(fails ? 1 : 0); |
