Merge branch 'dev': cross-subdomain passkey hint
CI/CD / deploy (push) Successful in 3m3s
Details
CI/CD / deploy (push) Successful in 3m3s
Details
This commit is contained in:
commit
be762ba2e8
|
|
@ -283,6 +283,16 @@ function getAllKnownUsernames(): string[] {
|
|||
for (const a of accounts) add(a.username);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// 5. Cross-subdomain hint cookie on .rspace.online — fresh subdomain has no
|
||||
// localStorage yet; login-button writes username here on successful auth.
|
||||
try {
|
||||
const match = document.cookie.split(/;\s*/).find(c => c.startsWith("rspace_hint="));
|
||||
if (match) {
|
||||
const parsed = JSON.parse(decodeURIComponent(match.slice("rspace_hint=".length)));
|
||||
if (parsed?.username) add(parsed.username);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,17 @@ import { getVaultManager, resetVaultManager } from '../vault';
|
|||
import { syncWalletsOnLogin } from '../wallet-sync';
|
||||
|
||||
// ============================================================================
|
||||
// KNOWN ACCOUNTS (localStorage)
|
||||
// KNOWN ACCOUNTS (localStorage + .rspace.online hint cookie)
|
||||
// ============================================================================
|
||||
//
|
||||
// Cross-subdomain UX hint ONLY. The cookie carries username + optional
|
||||
// displayName so the passkey button can say "Sign in as jeff" on a freshly
|
||||
// visited subdomain. No session/JWT/credential ever crosses origins — the
|
||||
// actual auth is still a WebAuthn ceremony bound to RP ID rspace.online.
|
||||
|
||||
const KNOWN_ACCOUNTS_KEY = 'encryptid-known-accounts';
|
||||
const HINT_COOKIE = 'rspace_hint';
|
||||
const HINT_TTL_SECONDS = 60 * 60 * 24 * 30; // 30 days
|
||||
const ENCRYPTID_AUTH = ''; // same-origin — avoids cross-origin issues on Safari
|
||||
|
||||
interface KnownAccount {
|
||||
|
|
@ -32,21 +39,74 @@ interface KnownAccount {
|
|||
displayName?: string;
|
||||
}
|
||||
|
||||
function getKnownAccounts(): KnownAccount[] {
|
||||
function parentDomainForCookie(): string | null {
|
||||
if (typeof location === 'undefined') return null;
|
||||
const host = location.hostname;
|
||||
// Share across *.rspace.online. Localhost / other hosts → per-origin only.
|
||||
if (host === 'rspace.online' || host.endsWith('.rspace.online')) return 'rspace.online';
|
||||
return null;
|
||||
}
|
||||
|
||||
function readHintCookie(): KnownAccount | null {
|
||||
if (typeof document === 'undefined') return null;
|
||||
const match = document.cookie.split(/;\s*/).find(c => c.startsWith(`${HINT_COOKIE}=`));
|
||||
if (!match) return null;
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(KNOWN_ACCOUNTS_KEY) || '[]');
|
||||
} catch { return []; }
|
||||
const raw = decodeURIComponent(match.slice(HINT_COOKIE.length + 1));
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed.username === 'string') {
|
||||
return { username: parsed.username, displayName: parsed.displayName };
|
||||
}
|
||||
} catch { /* malformed — ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeHintCookie(account: KnownAccount): void {
|
||||
const domain = parentDomainForCookie();
|
||||
if (!domain) return; // dev / non-rspace host: skip cross-origin hint
|
||||
const value = encodeURIComponent(JSON.stringify({
|
||||
username: account.username,
|
||||
...(account.displayName ? { displayName: account.displayName } : {}),
|
||||
}));
|
||||
document.cookie = `${HINT_COOKIE}=${value}; Domain=.${domain}; Path=/; Max-Age=${HINT_TTL_SECONDS}; Secure; SameSite=Lax`;
|
||||
}
|
||||
|
||||
function clearHintCookie(): void {
|
||||
const domain = parentDomainForCookie();
|
||||
if (!domain) return;
|
||||
document.cookie = `${HINT_COOKIE}=; Domain=.${domain}; Path=/; Max-Age=0; Secure; SameSite=Lax`;
|
||||
}
|
||||
|
||||
function getKnownAccounts(): KnownAccount[] {
|
||||
let accounts: KnownAccount[] = [];
|
||||
try {
|
||||
accounts = JSON.parse(localStorage.getItem(KNOWN_ACCOUNTS_KEY) || '[]');
|
||||
} catch { accounts = []; }
|
||||
|
||||
// Fresh subdomain → localStorage empty; fall back to cross-subdomain hint.
|
||||
if (accounts.length === 0) {
|
||||
const hint = readHintCookie();
|
||||
if (hint) accounts = [hint];
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
function addKnownAccount(account: KnownAccount): void {
|
||||
const accounts = getKnownAccounts().filter(a => a.username !== account.username);
|
||||
const existing: KnownAccount[] = (() => {
|
||||
try { return JSON.parse(localStorage.getItem(KNOWN_ACCOUNTS_KEY) || '[]'); }
|
||||
catch { return []; }
|
||||
})();
|
||||
const accounts = existing.filter(a => a.username !== account.username);
|
||||
accounts.unshift(account); // most recent first
|
||||
localStorage.setItem(KNOWN_ACCOUNTS_KEY, JSON.stringify(accounts));
|
||||
writeHintCookie(account);
|
||||
}
|
||||
|
||||
function removeKnownAccount(username: string): void {
|
||||
const accounts = getKnownAccounts().filter(a => a.username !== username);
|
||||
localStorage.setItem(KNOWN_ACCOUNTS_KEY, JSON.stringify(accounts));
|
||||
const hint = readHintCookie();
|
||||
if (hint && hint.username === username) clearHintCookie();
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
|
|
|
|||
Loading…
Reference in New Issue