PWA for iPhone and iPad: PNG icons, splash screens, manifest; stay signed in for 180 days
build-container / build (push) Successful in 1m56s

- iOS ignores SVG touch icons: add 120/152/167/180 apple-touch-icons, 192/512 and maskable manifest icons,
  and 25 splash screens (every iPhone size, iPad portrait+landscape incl. 12.9")
- Icons and splash screens are public so iOS can fetch them without a session
- Manifest: id, scope, categories, shortcuts; iOS standalone polish (no callout, no overscroll)
- Sessions last SESSION_DAYS (default 180) and renew daily while in use
- tools/make-icons.py regenerates the artwork

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-20 15:44:39 -04:00
1 parent 916882e55d
commit 044589686c
47 files changed
+293 -16

No files matched your search

+14 -6
View File
@@ -29,7 +29,9 @@ const save = () => writeJson('passkeys.json', store);
const SETUP_CODE = process.env.HUB_SETUP_CODE || crypto.randomBytes(4).toString('hex');
if (!store.creds.length) console.log(`[auth] No passkeys yet. Setup code: ${SETUP_CODE}`);
const SESSION_DAYS = 30;
// Sessions last SESSION_DAYS and slide forward: using the app renews them, so you only sign in again after that long idle.
const SESSION_DAYS = Math.max(1, parseInt(process.env.SESSION_DAYS, 10) || 180);
const LIFETIME = SESSION_DAYS * 864e5;
const mac = v => crypto.createHmac('sha256', secret).update(v).digest('base64url');
const cookies = req => Object.fromEntries((req.headers.cookie || '').split(/; */).filter(Boolean).map(c => { const i = c.indexOf('='); return [c.slice(0, i), decodeURIComponent(c.slice(i + 1))]; }));
const isHttps = req => (req.headers['x-forwarded-proto'] || '') === 'https' || (process.env.PUBLIC_URL || '').startsWith('https://');
@@ -40,9 +42,15 @@ function isAuthed(req) {
const want = mac('s' + exp);
return sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want)) && Number(exp) > Date.now();
}
function startSession(req, res) {
const exp = Date.now() + SESSION_DAYS * 864e5;
res.setHeader('Set-Cookie', `hub_session=${exp}.${mac('s' + exp)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_DAYS * 86400}${isHttps(req) ? '; Secure' : ''}`);
const sessionCookie = req => { const exp = Date.now() + LIFETIME;
return `hub_session=${exp}.${mac('s' + exp)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_DAYS * 86400}${isHttps(req) ? '; Secure' : ''}`; };
// Add a Set-Cookie without clobbering ones other code already set on this response.
const addCookie = (res, c) => { const cur = res.getHeader('Set-Cookie'); res.setHeader('Set-Cookie', cur ? [].concat(cur, c) : c); };
function startSession(req, res) { addCookie(res, sessionCookie(req)); }
/** Renew the session cookie when it is more than a day old, so an active session never runs out. */
function refresh(req, res) {
const [exp] = String(cookies(req).hub_session || '').split('.');
if (Number(exp) - Date.now() < LIFETIME - 864e5) addCookie(res, sessionCookie(req));
}
const challenges = new Map(); // cid -> { challenge, kind, exp }
@@ -100,7 +108,7 @@ async function handle(req, res) {
try {
if (p === '/api/auth/state') return send(res, 200, { registered: store.creds.length > 0, authed: isAuthed(req) });
if (p === '/api/auth/logout' && req.method === 'POST') {
res.setHeader('Set-Cookie', 'hub_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
addCookie(res, 'hub_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
return send(res, 200, { ok: true });
}
const body = req.method === 'POST' || req.method === 'DELETE' ? JSON.parse((await readBody(req)) || '{}') : {};
@@ -177,4 +185,4 @@ async function handle(req, res) {
} catch (e) { send(res, 400, { error: e.message }); }
}
module.exports = { handle, isAuthed, secret };
module.exports = { handle, isAuthed, refresh, addCookie, secret, SESSION_DAYS };