eleven / ocho Public

web: a setup link from home doesn't ask you to sign in again (#1445)

Create a space from your signed-in Eleven page, tap "Set up your space",
and the new space led with the signup-first card — asking you to sign in
one tap after you signed in, to save a space home had already saved.

offerPrejoin never looked at ?src=home, the marker home stamps on the
handover link. Now it does, so a from-home setup link falls straight
through to Eleven's setup conversation. The sign-in that link genuinely
owes — adding this SPACE'S KEY to the account — is unchanged, at the
conversation's keys-home step.


Claude-Session: https://claude.ai/code/session_01GUA1EJFYwgr2nZNVBRFf8k
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 5d6cc29911d15792f641f9f304c15d750014304b parent ccb775d
5 files changed, +33 −6
  • changelog/2026-08-28-no-second-signin-on-setup.md +3 −0
  • docs/superpowers/specs/2026-07-31-signup-first-join-design.md +5 −0
  • test/prejoin.test.mjs +9 −0
  • web/static/join.js +7 −4
  • web/static/prejoin.js +9 −2
diff --git a/changelog/2026-08-28-no-second-signin-on-setup.md b/changelog/2026-08-28-no-second-signin-on-setup.md
new file mode 100644
index 0000000..de212dc
--- /dev/null
+++ b/changelog/2026-08-28-no-second-signin-on-setup.md
@@ -0,0 +1,3 @@
+🤖 Making a new space no longer asks you to sign in twice. Come from your
+Eleven page and your new space opens straight into setting it up — the space
+is already saved to your account, so there's nothing to sign into again.
diff --git a/docs/superpowers/specs/2026-07-31-signup-first-join-design.md b/docs/superpowers/specs/2026-07-31-signup-first-join-design.md
index fb96790..0076012 100644
--- a/docs/superpowers/specs/2026-07-31-signup-first-join-design.md
+++ b/docs/superpowers/specs/2026-07-31-signup-first-join-design.md
@@ -48,6 +48,11 @@ These paths fall through to today's behaviour untouched:
- Dead/consumed invites — the existing error card (with its sign-in door).
- Instances with no home server configured (`__HOME_ORIGIN__` empty —
self-hosters).
+- 🤖 Setup links opened from home's create-a-space flow (`?src=home`): that
+ person signed in at home one tap ago, and home has already saved the new
+ space to their account. The sign-in they still owe — adding this space's
+ KEY to that account — is the setup conversation's `keys-home` step, at the
+ end.
## 3. The home-side pre-join intent
diff --git a/test/prejoin.test.mjs b/test/prejoin.test.mjs
index 6a55b4b..4cd3989 100644
--- a/test/prejoin.test.mjs
+++ b/test/prejoin.test.mjs
@@ -24,6 +24,15 @@ test("offerPrejoin: never for established or unsupported contexts", () => {
assert.equal(offerPrejoin({ ...base, signedUp: true }), false);
});
+// The create-a-space journey: home hands over /join/setup?src=home moments
+// after a sign-in there (and having already saved the space to that account).
+// Leading with the sign-in door again reads as the flow forgetting.
+test("offerPrejoin: a setup link from a signed-in home page never leads with sign-in", () => {
+ assert.equal(offerPrejoin({ ...base, kind: "setup", fromHome: true }), false);
+ assert.equal(offerPrejoin({ ...base, kind: "join", fromHome: true }), false);
+ assert.equal(offerPrejoin({ ...base, kind: "setup", fromHome: false }), true);
+});
+
test("round trip: stash then read back on a genuine return", () => {
const now = 1_000_000;
const stored = prejoinStash({ frag: "e1.SECRET~Ann~bkey", nonce: "n0nce", now });
diff --git a/web/static/join.js b/web/static/join.js
index 5c8a75e..af479e9 100644
--- a/web/static/join.js
+++ b/web/static/join.js
@@ -68,10 +68,12 @@ const prejoin = (() => {
const { T, name: fragName, b: fragB } = parseInviteFragment(location.hash.slice(1) || prejoin.frag);
// ?src=home marks a link opened from the person's Eleven sign-in page (the
-// create-a-space flow). Remember it on this origin so the setup conversation's
-// key step can say "add to your passkey" instead of pitching passkeys from
-// scratch. A hint only — nothing downstream trusts it for access.
-if (new URLSearchParams(location.search).get("src") === "home") {
+// create-a-space flow). Two jobs, neither of them access: it stands the
+// signup-first lead card down (they signed in one tap ago — offerPrejoin), and
+// it's remembered on this origin so the setup conversation's key step can say
+// "add to your account" instead of pitching a sign-in from scratch.
+const fromHome = new URLSearchParams(location.search).get("src") === "home";
+if (fromHome) {
try { localStorage.setItem("lchat_from_home", "1"); } catch { /* storage denied: the generic copy shows */ }
}
@@ -358,6 +360,7 @@ function showWelcome(kind, onContinue, spaceDesc = "", opts = {}) {
embed: !!window.__lchatEmbed,
native: document.documentElement.classList.contains("native"),
signedUp: prejoin.signedUp,
+ fromHome, // came straight from a signed-in home page — don't ask twice
})) {
return showPrejoin(kind, onContinue, spaceDesc);
}
diff --git a/web/static/prejoin.js b/web/static/prejoin.js
index a8012a5..2b4b547 100644
--- a/web/static/prejoin.js
+++ b/web/static/prejoin.js
@@ -15,8 +15,15 @@ const MAX_AGE_MS = 60 * 60 * 1000;
// device links and adopts already have an identity), only where a home server
// exists to sign into, and never in the native embed/wrapper (the apps own
// their sign-in). signedUp means the round trip already happened.
-export function offerPrejoin({ kind, homeOrigin, embed, native, signedUp }) {
- if (embed || native || signedUp) return false;
+//
+// fromHome (the setup link's ?src=home) is the other established identity:
+// this person just created the space FROM their signed-in Eleven page, which
+// already saved it to their account — asking them to sign in again, one tap
+// after signing in, is the flow contradicting itself. They meet the sign-in
+// once more at the end, where it belongs: the setup conversation's keys-home
+// step, adding this space's key to the account they already have.
+export function offerPrejoin({ kind, homeOrigin, embed, native, signedUp, fromHome }) {
+ if (embed || native || signedUp || fromHome) return false;
if (kind !== "join" && kind !== "setup") return false;
return typeof homeOrigin === "string" && homeOrigin.startsWith("http");
}