feat: move EncryptID to auth.rspace.online, rebrand as rStack Identity

Traefik routes auth.rspace.online (priority 150) with encryptid.jeffemmett.com
fallback. Landing page rebranded as rStack Identity with rStack.online ecosystem
tagline. Registration form now includes optional email for account recovery.
JWT issuer and recovery URL updated. 14 r* apps listed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-18 10:49:35 +00:00
parent fa80968b7f
commit e65cfffefd
2 changed files with 46 additions and 28 deletions

View File

@ -21,12 +21,13 @@ services:
- SMTP_USER=${SMTP_USER:-noreply@jeffemmett.com} - SMTP_USER=${SMTP_USER:-noreply@jeffemmett.com}
- SMTP_PASS=${SMTP_PASS} - SMTP_PASS=${SMTP_PASS}
- SMTP_FROM=${SMTP_FROM:-EncryptID <noreply@jeffemmett.com>} - SMTP_FROM=${SMTP_FROM:-EncryptID <noreply@jeffemmett.com>}
- RECOVERY_URL=${RECOVERY_URL:-https://encryptid.jeffemmett.com/recover} - RECOVERY_URL=${RECOVERY_URL:-https://auth.rspace.online/recover}
labels: labels:
# Traefik auto-discovery # Traefik auto-discovery
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.encryptid.rule=Host(`encryptid.jeffemmett.com`)" - "traefik.http.routers.encryptid.rule=Host(`auth.rspace.online`) || Host(`encryptid.jeffemmett.com`)"
- "traefik.http.routers.encryptid.entrypoints=web" - "traefik.http.routers.encryptid.entrypoints=web"
- "traefik.http.routers.encryptid.priority=150"
- "traefik.http.services.encryptid.loadbalancer.server.port=3000" - "traefik.http.services.encryptid.loadbalancer.server.port=3000"
# Also serve from root domain for .well-known (WebAuthn Related Origins) # Also serve from root domain for .well-known (WebAuthn Related Origins)
- "traefik.http.routers.encryptid-wellknown.rule=Host(`jeffemmett.com`) && PathPrefix(`/.well-known/webauthn`)" - "traefik.http.routers.encryptid-wellknown.rule=Host(`jeffemmett.com`) && PathPrefix(`/.well-known/webauthn`)"

View File

@ -64,8 +64,9 @@ const CONFIG = {
pass: process.env.SMTP_PASS || '', pass: process.env.SMTP_PASS || '',
from: process.env.SMTP_FROM || 'EncryptID <noreply@jeffemmett.com>', from: process.env.SMTP_FROM || 'EncryptID <noreply@jeffemmett.com>',
}, },
recoveryUrl: process.env.RECOVERY_URL || 'https://encryptid.jeffemmett.com/recover', recoveryUrl: process.env.RECOVERY_URL || 'https://auth.rspace.online/recover',
allowedOrigins: [ allowedOrigins: [
'https://auth.rspace.online',
'https://encryptid.jeffemmett.com', 'https://encryptid.jeffemmett.com',
'https://jeffemmett.com', 'https://jeffemmett.com',
'https://rspace.online', 'https://rspace.online',
@ -132,11 +133,11 @@ async function sendRecoveryEmail(to: string, token: string, username: string): P
await smtpTransport.sendMail({ await smtpTransport.sendMail({
from: CONFIG.smtp.from, from: CONFIG.smtp.from,
to, to,
subject: 'EncryptID — Account Recovery', subject: 'rStack — Account Recovery',
text: [ text: [
`Hi ${username},`, `Hi ${username},`,
'', '',
'A recovery request was made for your EncryptID account.', 'A recovery request was made for your rStack account.',
'Use the link below to add a new passkey:', 'Use the link below to add a new passkey:',
'', '',
recoveryLink, recoveryLink,
@ -144,7 +145,7 @@ async function sendRecoveryEmail(to: string, token: string, username: string): P
'This link expires in 30 minutes.', 'This link expires in 30 minutes.',
'If you did not request this, you can safely ignore this email.', 'If you did not request this, you can safely ignore this email.',
'', '',
'— EncryptID', '— rStack Identity',
].join('\n'), ].join('\n'),
html: ` html: `
<!DOCTYPE html> <!DOCTYPE html>
@ -156,11 +157,11 @@ async function sendRecoveryEmail(to: string, token: string, username: string): P
<table width="480" cellpadding="0" cellspacing="0" style="background:#16213e;border-radius:12px;border:1px solid rgba(255,255,255,0.1);"> <table width="480" cellpadding="0" cellspacing="0" style="background:#16213e;border-radius:12px;border:1px solid rgba(255,255,255,0.1);">
<tr><td style="padding:32px 32px 24px;text-align:center;"> <tr><td style="padding:32px 32px 24px;text-align:center;">
<div style="font-size:36px;margin-bottom:8px;">&#128274;</div> <div style="font-size:36px;margin-bottom:8px;">&#128274;</div>
<h1 style="margin:0;font-size:24px;background:linear-gradient(90deg,#00d4ff,#7c3aed);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">EncryptID</h1> <h1 style="margin:0;font-size:24px;background:linear-gradient(90deg,#00d4ff,#7c3aed);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">rStack Identity</h1>
</td></tr> </td></tr>
<tr><td style="padding:0 32px 24px;color:#e2e8f0;font-size:15px;line-height:1.6;"> <tr><td style="padding:0 32px 24px;color:#e2e8f0;font-size:15px;line-height:1.6;">
<p>Hi <strong>${username}</strong>,</p> <p>Hi <strong>${username}</strong>,</p>
<p>A recovery request was made for your EncryptID account. Click below to add a new passkey:</p> <p>A recovery request was made for your rStack account. Click below to add a new passkey:</p>
</td></tr> </td></tr>
<tr><td style="padding:0 32px 32px;text-align:center;"> <tr><td style="padding:0 32px 32px;text-align:center;">
<a href="${recoveryLink}" style="display:inline-block;padding:12px 32px;background:linear-gradient(90deg,#00d4ff,#7c3aed);color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;">Recover Account</a> <a href="${recoveryLink}" style="display:inline-block;padding:12px 32px;background:linear-gradient(90deg,#00d4ff,#7c3aed);color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;">Recover Account</a>
@ -281,7 +282,7 @@ app.post('/api/register/start', async (c) => {
* Complete registration - verify and store credential * Complete registration - verify and store credential
*/ */
app.post('/api/register/complete', async (c) => { app.post('/api/register/complete', async (c) => {
const { challenge, credential, userId, username } = await c.req.json(); const { challenge, credential, userId, username, email } = await c.req.json();
// Verify challenge // Verify challenge
const challengeRecord = await getChallenge(challenge); const challengeRecord = await getChallenge(challenge);
@ -301,6 +302,11 @@ app.post('/api/register/complete', async (c) => {
const did = `did:key:${userId.slice(0, 32)}`; const did = `did:key:${userId.slice(0, 32)}`;
await createUser(userId, username, username, did); await createUser(userId, username, username, did);
// Set recovery email if provided during registration
if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
await setUserEmail(userId, email);
}
const storedCredential: StoredCredential = { const storedCredential: StoredCredential = {
credentialId: credential.credentialId, credentialId: credential.credentialId,
publicKey: credential.publicKey, publicKey: credential.publicKey,
@ -643,7 +649,7 @@ async function generateSessionToken(userId: string, username: string): Promise<s
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const payload = { const payload = {
iss: 'https://encryptid.jeffemmett.com', iss: 'https://auth.rspace.online',
sub: userId, sub: userId,
aud: CONFIG.allowedOrigins, aud: CONFIG.allowedOrigins,
iat: now, iat: now,
@ -674,7 +680,7 @@ app.get('/recover', (c) => {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EncryptID Account Recovery</title> <title>rStack Identity Account Recovery</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@ -724,7 +730,7 @@ app.get('/recover', (c) => {
<div class="card"> <div class="card">
<div class="logo">&#128274;</div> <div class="logo">&#128274;</div>
<h1>Account Recovery</h1> <h1>Account Recovery</h1>
<p class="subtitle">Verify your recovery link and add a new passkey</p> <p class="subtitle">Verify your recovery link and add a new passkey to your rStack account</p>
<div id="status" class="status loading">Verifying recovery token...</div> <div id="status" class="status loading">Verifying recovery token...</div>
@ -967,7 +973,7 @@ app.get('/', (c) => {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EncryptID - Unified Identity for the r-Ecosystem</title> <title>rStack Identity One Passkey for the r* Ecosystem</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
@ -1082,8 +1088,8 @@ app.get('/', (c) => {
<body> <body>
<div class="page"> <div class="page">
<div class="header"> <div class="header">
<h1>EncryptID</h1> <h1>rStack Identity</h1>
<p class="tagline">Unified Identity for the r-Ecosystem</p> <p class="tagline">One passkey for the <a href="https://rstack.online" style="color:#00d4ff;text-decoration:none">rStack.online</a> ecosystem</p>
</div> </div>
<!-- Auth card: login/register when signed out, profile when signed in --> <!-- Auth card: login/register when signed out, profile when signed in -->
@ -1091,7 +1097,7 @@ app.get('/', (c) => {
<!-- Login/Register form --> <!-- Login/Register form -->
<div id="auth-form"> <div id="auth-form">
<h2>Get started</h2> <h2>Get started</h2>
<p class="sub">Sign in with your passkey or create a new account. No passwords, no tracking.</p> <p class="sub">Sign in with your passkey or create a new account. No passwords, no tracking, no third parties.</p>
<div class="tabs"> <div class="tabs">
<button class="tab active" id="tab-signin" onclick="switchTab('signin')">Sign In</button> <button class="tab active" id="tab-signin" onclick="switchTab('signin')">Sign In</button>
@ -1107,6 +1113,10 @@ app.get('/', (c) => {
<label for="username">Username</label> <label for="username">Username</label>
<input id="username" type="text" placeholder="Choose a username" autocomplete="username" /> <input id="username" type="text" placeholder="Choose a username" autocomplete="username" />
</div> </div>
<div class="form-group">
<label for="reg-email">Email <span style="color:#475569;font-weight:400">(optional for account recovery)</span></label>
<input id="reg-email" type="email" placeholder="you@example.com" autocomplete="email" />
</div>
</div> </div>
<button class="btn-primary" id="auth-btn" onclick="handleAuth()">Sign In with Passkey</button> <button class="btn-primary" id="auth-btn" onclick="handleAuth()">Sign In with Passkey</button>
@ -1153,8 +1163,8 @@ app.get('/', (c) => {
<div class="feature-desc">Hardware-backed, phishing-resistant login. No passwords ever.</div> <div class="feature-desc">Hardware-backed, phishing-resistant login. No passwords ever.</div>
</div> </div>
<div class="feature"> <div class="feature">
<div class="feature-title">Social Recovery</div> <div class="feature-title">Email Recovery</div>
<div class="feature-desc">Recover your account via email. No seed phrases needed.</div> <div class="feature-desc">Optional email for account recovery. No seed phrases needed.</div>
</div> </div>
<div class="feature"> <div class="feature">
<div class="feature-title">E2E Encryption</div> <div class="feature-title">E2E Encryption</div>
@ -1162,22 +1172,26 @@ app.get('/', (c) => {
</div> </div>
<div class="feature"> <div class="feature">
<div class="feature-title">Cross-App Identity</div> <div class="feature-title">Cross-App Identity</div>
<div class="feature-desc">One passkey works across every app in the r-suite.</div> <div class="feature-desc">One passkey works across every app in the rStack ecosystem.</div>
</div> </div>
</div> </div>
<div class="apps"> <div class="apps">
<div class="apps-title">One identity across the r-Ecosystem</div> <div class="apps-title">One identity across the <a href="https://rstack.online" style="color:#64748b;text-decoration:none">rStack.online</a> ecosystem</div>
<div class="app-links"> <div class="app-links">
<a href="https://rspace.online">rSpace</a> <a href="https://rspace.online">rSpace</a>
<a href="https://rnotes.online">rNotes</a> <a href="https://rnotes.online">rNotes</a>
<a href="https://rfiles.online">rFiles</a> <a href="https://rfiles.online">rFiles</a>
<a href="https://rcart.online">rCart</a> <a href="https://rcart.online">rCart</a>
<a href="https://rfunds.online">rFunds</a> <a href="https://rfunds.online">rFunds</a>
<a href="https://rwallet.online">rWallet</a>
<a href="https://rauctions.online">rAuctions</a> <a href="https://rauctions.online">rAuctions</a>
<a href="https://rpubs.online">rPubs</a> <a href="https://rpubs.online">rPubs</a>
<a href="https://rvote.online">rVote</a> <a href="https://rvote.online">rVote</a>
<a href="https://rmaps.online">rMaps</a> <a href="https://rmaps.online">rMaps</a>
<a href="https://rtrips.online">rTrips</a>
<a href="https://rtube.online">rTube</a>
<a href="https://rinbox.online">rInbox</a>
<a href="https://rstack.online">rStack</a> <a href="https://rstack.online">rStack</a>
</div> </div>
</div> </div>
@ -1228,20 +1242,23 @@ app.get('/', (c) => {
if (currentMode === 'register') { if (currentMode === 'register') {
const username = document.getElementById('username').value.trim(); const username = document.getElementById('username').value.trim();
if (!username) { showError('Username is required'); btn.disabled = false; return; } if (!username) { showError('Username is required'); btn.disabled = false; return; }
const email = document.getElementById('reg-email').value.trim();
btn.textContent = 'Creating passkey...'; btn.textContent = 'Creating passkey...';
const credential = await registerPasskey(username, username); const credential = await registerPasskey(username, username);
// Complete registration with server // Complete registration with server
const res = await fetch('/api/register/complete', { const regBody = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge: credential.challenge, challenge: credential.challenge,
credential: credential.credential, credential: credential.credential,
userId: credential.userId, userId: credential.userId,
username, username,
}), };
if (email) regBody.email = email;
const res = await fetch('/api/register/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(regBody),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Registration failed'); if (!res.ok) throw new Error(data.error || 'Registration failed');
@ -1386,9 +1403,9 @@ setInterval(() => {
console.log(` console.log(`
🔐 EncryptID Server 🔐 rStack Identity (EncryptID)
Unified Identity for the r-Ecosystem One passkey for the rStack.online ecosystem
Port: ${CONFIG.port} Port: ${CONFIG.port}
RP ID: ${CONFIG.rpId} RP ID: ${CONFIG.rpId}