3, "wing" => "Identity", "kind" => "human", "title" => "Sign out everywhere", "promise" => "Signing out at account signs you out of every app in this browser.", "block" => "rc_sign_out() sends the browser to account, which ends the one session behind every app's token. No app has to be told.", "show" => function (?array $me): string { if ($me) return '

Press this, confirm at account, and come back: every "who am I" card (here and on ' . 'the second app) should say visitor.

' . '

'; [$uid, $t, $sig] = array_pad(explode(".", (string)($_COOKIE["__Host-zoo_out"] ?? "")), 3, ""); if ($sig !== "" && hash_equals(hash_hmac("sha256", "$uid.$t", signout_mark()), $sig) && time() - (int)$t < 3600) { $u = rc_users([(int)$uid])[(int)$uid] ?? null; return '

You signed out as @' . h($u["username"] ?? "?") . '. Does every card say visitor, here and on the second app?

' . '

'; } return '

Sign in first; then this button signs you out of every app at once.

'; }, "api" => function (string $do, ?array $me, array $in, bool $post): ?array { if ($do === "signout") { if ($me) { $t = time(); setcookie("__Host-zoo_out", "{$me['id']}.$t." . hash_hmac("sha256", "{$me['id']}.$t", signout_mark()), ["expires" => $t + 3600, "path" => "/", "secure" => true, "httponly" => true, "samesite" => "Lax"]); } rc_sign_out("https://" . env("RC_HOST") . "/#e3"); } if ($do === "confirm" && $post) { [$uid, $t, $sig] = array_pad(explode(".", (string)($_COOKIE["__Host-zoo_out"] ?? "")), 3, ""); if ($me) return ["error" => "you are signed in again: this card says your name", "status" => 409]; if ($sig === "" || !hash_equals(hash_hmac("sha256", "$uid.$t", signout_mark()), $sig) || time() - (int)$t > 3600) return ["error" => "no recent sign-out from this browser", "status" => 400]; seen(3, ["id" => (int)$uid]); setcookie("__Host-zoo_out", "", ["expires" => 1, "path" => "/", "secure" => true, "httponly" => true, "samesite" => "Lax"]); return ["ok" => true]; } return null; }, "script" => <<<'JS' document.querySelector(".seen-out")?.addEventListener("click", async (ev) => { ev.target.disabled = true; const j = await zoo.call("/api/3/confirm", {}); ev.target.textContent = j.error || "Recorded, thank you"; if (!j.error) setTimeout(() => location.reload(), 600); }); JS, ];