Demo images / Build & push demo images (push) Successful in 3m49s
An app whose frame never loads while its backend reports Running (the embed-refusal signature: frame-busting JS, top-level-origin apps, SameSite=Strict logins — everything the gate's header stripping cannot fix) is remembered in localStorage; every later launch opens a tab straight from the click (user gesture, so no popup blocker), and opensInTab() gives it the tab-launch icon. A successful iframe load clears the memory and entries expire after 7 days, so nodes that gain embedding (gate improvements) get re-probed instead of being remembered broken forever. Dev guide updated; v1.8.2 changelog + What's New curated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
/**
|
|
* Auto-tab memory — apps observed to refuse iframe embedding.
|
|
*
|
|
* The gate strips frame-blocking headers for gated apps (1.8.1+), but three
|
|
* failure modes survive any header fix: JS frame-busting, apps that must be
|
|
* the top-level origin (OAuth/WebAuthn), and SameSite=Strict session cookies.
|
|
* Cross-origin embed failure is not reliably detectable up front from the
|
|
* browser, so the app session's load-timeout is the detector — and this
|
|
* module is the memory: once an app is seen blocked while RUNNING, it is
|
|
* remembered here and every later launch opens it as a tab app directly from
|
|
* the click (a user gesture, so never popup-blocked). The dead grey pane
|
|
* happens at most once per app.
|
|
*
|
|
* Entries expire after 7 days so a node update that fixes embedding (for
|
|
* example a gate improvement) gets re-probed instead of being remembered as
|
|
* broken forever; a successful iframe load also clears the entry immediately.
|
|
*/
|
|
|
|
const KEY = 'archipelago_auto_new_tab_apps'
|
|
const EXPIRY_MS = 7 * 24 * 60 * 60 * 1000
|
|
|
|
function read(): Record<string, number> {
|
|
try {
|
|
const raw = JSON.parse(localStorage.getItem(KEY) || '{}')
|
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) return raw
|
|
} catch { /* corrupt/unavailable storage reads as empty */ }
|
|
return {}
|
|
}
|
|
|
|
function write(entries: Record<string, number>) {
|
|
try { localStorage.setItem(KEY, JSON.stringify(entries)) } catch { /* full/denied */ }
|
|
}
|
|
|
|
export function isAutoTabApp(id: string): boolean {
|
|
const at = read()[id]
|
|
return typeof at === 'number' && Date.now() - at < EXPIRY_MS
|
|
}
|
|
|
|
export function rememberAutoTabApp(id: string): void {
|
|
const entries = read()
|
|
entries[id] = Date.now()
|
|
write(entries)
|
|
}
|
|
|
|
export function forgetAutoTabApp(id: string): void {
|
|
const entries = read()
|
|
if (id in entries) {
|
|
delete entries[id]
|
|
write(entries)
|
|
}
|
|
}
|