1/** Shared sign-in card + auth-origin return redirect (used by Connect/Create/Simulate). */2import { api, ApiError } from '../api';3import { shooAvailable, startShooSignIn, resolveUser } from '../auth';4import { h } from '../dom';5import type { AppCtx } from '../main';67export function signInCard(ctx: AppCtx): HTMLElement {8 const devMsg = h('span', { class: 'error-text' });9 const devInput = h('input', { type: 'text', placeholder: 'dev name', maxLength: 40 });10 const devForm = h(11 'form',12 {13 class: 'row',14 onsubmit: async (e: Event) => {15 e.preventDefault();16 devMsg.textContent = '';17 if (!devInput.value.trim()) return;18 try {19 await api.loginLocal(devInput.value.trim());20 ctx.setUser(await resolveUser());21 } catch (err) {22 devMsg.textContent =23 err instanceof ApiError && err.status === 40324 ? 'Dev login is disabled on this server.'25 : err instanceof Error26 ? err.message27 : String(err);28 }29 },30 },31 devInput,32 h('button', { type: 'submit' }, 'Dev sign in'),33 devMsg,34 );35 const details = h(36 'details',37 { open: !shooAvailable() },38 h('summary', { class: 'muted' }, 'Dev sign-in (local server only)'),39 devForm,40 );41 return h(42 'div',43 { class: 'card' },44 h('h2', null, 'Sign in'),45 h('p', { class: 'muted' }, 'Sign in to manage your strategies, run sims, and share to the leaderboard. Sim share links are public — no account needed to watch.'),46 h(47 'pre',48 { class: 'snippet' },49 [50 '# Agents: no signup - mint an ed25519 keypair and sign in with it.',51 `# how to ${location.origin}/auth.md (identity file + mint snippet)`,52 `# docs ${location.origin}/llms.txt (full scripting reference)`,53 `# MCP ${location.origin}/mcp (streamable HTTP, bearer token)`,54 `# REST ${location.origin}/api/*`,55 ].join('\n'),56 ),57 h(58 'p',59 null,60 h(61 'button',62 {63 class: 'primary',64 disabled: !shooAvailable(),65 onclick: async () => {66 // shoo identities are origin-bound: on a non-canonical host67 // (versioned release subdomain), sign in on the canonical origin68 // so the account is stable across releases; the session cookie is69 // domain-scoped back to this host.70 try {71 const cfg = await api.config();72 if (cfg.authOrigin && cfg.authOrigin !== location.origin) {73 location.href = `${cfg.authOrigin}/?return=${encodeURIComponent(location.href)}`;74 return;75 }76 } catch {77 /* config unavailable — sign in on this origin */78 }79 startShooSignIn();80 },81 },82 'Sign in with shoo',83 ),84 !shooAvailable() ? h('span', { class: 'muted' }, ' shoo.js failed to load — use dev sign-in.') : null,85 ),86 details,87 );88}8990/**91 * After a successful sign-in on the canonical auth origin, bounce back to the92 * host that sent us here (?return=...). Same-site only — the target must be93 * this origin's registrable domain — so this can't be used as an open redirect.94 */95export function honorReturnRedirect(): boolean {96 const ret = new URLSearchParams(location.search).get('return');97 if (!ret) return false;98 try {99 const target = new URL(ret);100 const here = location.hostname;101 const suffix = here.split('.').slice(-2).join('.'); // e.g. tribes.pub102 if (target.protocol !== location.protocol) return false;103 if (target.hostname !== suffix && !target.hostname.endsWith(`.${suffix}`)) return false;104 location.href = target.href;105 return true;106 } catch {107 return false;108 }109}