tools: fix dev-up's reissued sign-in link dying with 'invite was just used'
On a REUSED instance, reissueYouInvite re-minted your add-device link and then patched the group's thread_id onto it (the pre-open nicety). Since the invite-takeover guard landed (claimKeyInvite refuses a thread-bound invite once its user already has a device), that combination is exactly what the server rejects — so every restart's printed link failed with 'This invite was just used. Ask for a new one.' on any machine with the sqlite3 CLI present. Reissued links now skip the thread patch: they sign you in and leave the group one tap away (the printout says so honestly). The fresh-seed link keeps the pre-open — its first claim happens before 'you' has a device, which the guard allows. Verified both paths against a live dev instance, simulating the sqlite3 patch where this container lacks the CLI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdpyYWMyEhubwEPEWNmJZG
1 file changed,
+12
−4
tools/lchat-dev-up.mjs+12 −4
diff --git a/tools/lchat-dev-up.mjs b/tools/lchat-dev-up.mjs| index 4d3ded3..7d71fb4 100755 |
| --- a/tools/lchat-dev-up.mjs |
| +++ b/tools/lchat-dev-up.mjs |
| @@ -252,7 +252,14 @@ async function seed(base, adminToken) { |
| // the keypair we stashed in the config on the fresh seed. Passing the existing |
| // user_id makes the server re-issue for that member rather than create a new one |
| // (see createKeyInvite). Returns the new invite id + URL. |
| -async function reissueYouInvite(base, adminToken, cfg, dbPath) { |
| +// |
| +// Deliberately NOT thread-patched: claimKeyInvite refuses a thread-bound |
| +// invite once its user already has a device (the anti-account-takeover guard |
| +// on user invites), and a reused instance's "you" always has one — a patched |
| +// reissue would make the printed link die with "This invite was just used." |
| +// The pre-open nicety only works on the very first, fresh-seed claim; a |
| +// reissued link signs you in and leaves the group one tap away. |
| +async function reissueYouInvite(base, adminToken, cfg) { |
| const admin = { "Content-Type": "application/json", Authorization: `Bearer ${adminToken}` }; |
| const youT = randomSecret(); |
| const derived = await deriveInvite(youT); |
| @@ -262,7 +269,6 @@ async function reissueYouInvite(base, adminToken, cfg, dbPath) { |
| { id: derived.id, wrapped, claim_hash: derived.claimHash, user_id: cfg.you.userId }, |
| admin, |
| ); |
| - if (cfg.threadId) patchInviteThread(dbPath, derived.id, cfg.threadId); |
| return { |
| inviteId: derived.id, |
| inviteURL: `${base}/join/join#${buildInviteFragment(youT, "You")}`, |
| @@ -439,7 +445,7 @@ async function main() { |
| inviteType = "SETUP (already claimed — reload your browser)"; |
| } else { |
| log("reusing existing instance — re-minting a sign-in link for you..."); |
| - const re = await reissueYouInvite(base, adminToken, existing, dbPath); |
| + const re = await reissueYouInvite(base, adminToken, existing); |
| cfg.inviteId = re.inviteId; |
| cfg.inviteURL = re.inviteURL; |
| inviteType = "Regular (user invite)"; |
| @@ -499,9 +505,11 @@ async function main() { |
| } else if (onboarding) { |
| console.log("📋 Open the invite link to become admin and start onboarding."); |
| console.log(" The /admin command will be available in chat after you join."); |
| + } else if (reusing) { |
| + console.log("💬 Open the invite link to sign in — the group with Dev Buddy is one tap away."); |
| + console.log(" (Already signed in from before? Just reload — the link is only if you need it.)"); |
| } else { |
| console.log("💬 Open the invite link to land straight in the group chat with Dev Buddy."); |
| - if (reusing) console.log(" (Already signed in from before? Just reload — the link is only if you need it.)"); |
| } |
| console.log(""); |
| if (hooksSidecar) { |