eleven / messenger Public

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
Claude bdf08643436a4469072eb7d7247d2a85c6c2b1d0 parent 9b26418
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) {