Full Code of DosX-dev/rep3 for AI

main 13f5239a8186 cached
20 files
915.8 KB
221.5k tokens
344 symbols
1 requests
Download .txt
Showing preview only (952K chars total). Download the full file or copy to clipboard to get everything.
Repository: DosX-dev/rep3
Branch: main
Commit: 13f5239a8186
Files: 20
Total size: 915.8 KB

Directory structure:
gitextract_3vpkqevd/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY_AUDIT.md
└── src/
    ├── .server.ps1
    ├── css/
    │   └── app.css
    ├── index.html
    └── js/
        ├── constants.js
        ├── crypto.js
        ├── db.js
        ├── desktop.js
        ├── detectors/
        │   └── incognito.js
        ├── docmode.js
        ├── fileops.js
        ├── home.js
        ├── initlog.js
        ├── main.js
        ├── proactive/
        │   └── daemon.js
        ├── state.js
        └── vfs.js

================================================
FILE CONTENTS
================================================

================================================
FILE: CONTRIBUTING.md
================================================
> Thank you for considering a contribution to **SafeNova**. This document explains how to do it properly so your effort isn't wasted and the review process goes smoothly.

<a id="toc"></a>

## 📚 Table of Contents

-   [🧭 Before you start](#before-you-start)
-   [🐛 Reporting bugs](#reporting-bugs)
-   [💡 Suggesting features](#suggesting-features)
-   [🔀 Submitting a pull request](#submitting-a-pull-request)
    -   [Setting up the environment](#setup)
    -   [Branch naming](#branch-naming)
    -   [Commit messages](#commit-messages)
    -   [Pull request checklist](#pr-checklist)
-   [🎨 Code style](#code-style)
    -   [General rules](#style-general)
    -   [JavaScript specifics](#style-js)
    -   [HTML & CSS](#style-html-css)
-   [🔐 Security contribution rules](#security-rules)
-   [🚫 What we do NOT accept](#not-accepted)

---

<a id="before-you-start"></a>

## 🧭 Before you start

SafeNova is a security-first project. Before touching anything, spend time understanding how it actually works:

-   Read the full [README](./README.md) — especially the [SafeNova Proactive](./README.md#safenova-proactive), [Encryption](./README.md#encryption), and [How containers work](./README.md#how-containers-work) sections
-   Understand the [project structure](./README.md#project-structure) — each file has a specific, narrow responsibility
-   Look at the existing code style before writing a single line

> **The codebase is small and intentional.** There are no dead files, no legacy layers, no placeholder code. If something looks unusual, there is almost always a documented reason for it — read the surrounding comments before assuming it is wrong.

---

<a id="reporting-bugs"></a>

## 🐛 Reporting bugs

Use [GitHub Issues](https://github.com/DosX-dev/SafeNova/issues) to report bugs. Before opening a new issue:

-   Check if the issue already exists
-   Reproduce the bug on the latest version
-   Make sure it happens in a supported browser (Chrome 90+, Firefox 90+, Safari 15+, Edge 90+)

A good bug report includes:

| Field           | What to provide                                                               |
| --------------- | ----------------------------------------------------------------------------- |
| **Description** | What happened vs. what you expected                                           |
| **Steps**       | Exact numbered steps to reproduce                                             |
| **Environment** | Browser name + version, OS, online vs. local                                  |
| **Logs**        | DevTools console output if relevant — paste as text, not a screenshot         |
| **Severity**    | Does it cause data loss? Does it affect security? Does it only affect the UI? |

> **If the bug is security-related** (data exposure, bypass of any protection layer, key material leakage), do **not** file a public issue. See [Security contribution rules](#security-rules) below.

---

<a id="suggesting-features"></a>

## 💡 Suggesting features

Open a [GitHub Issue](https://github.com/DosX-dev/SafeNova/issues) with the `enhancement` label. Describe:

-   **What problem it solves** — not just what it does, but why it matters
-   **Who benefits** — casual user, power user, security-conscious user?
-   **Alternatives you considered** — shows you thought it through
-   **Any security implications** — SafeNova handles encrypted data; new features can introduce new attack surface

Features that don't have a clear security story or that add complexity without proportional value will likely be declined. That's not a rejection of effort — it's a design constraint.

---

<a id="submitting-a-pull-request"></a>

## 🔀 Submitting a pull request

<a id="setup"></a>

### Setting up the environment

There is no build step. The project runs as static files:

```powershell
# Clone the repo
git clone https://github.com/DosX-dev/SafeNova.git
cd SafeNova

# Start the local server
.\.server.ps1
```

The server starts on port `7777` (or the next free port) and opens the app in your browser. Edit files directly in `src/` — no bundler, no transpiler, no `npm install`.

<a id="branch-naming"></a>

### Branch naming

| Prefix      | Use for                                      | Example                          |
| ----------- | -------------------------------------------- | -------------------------------- |
| `fix/`      | Bug fixes                                    | `fix/export-blob-url`            |
| `feature/`  | New functionality                            | `feature/keyboard-shortcut-copy` |
| `refactor/` | Code cleanup with no behavior change         | `refactor/vfs-node-validation`   |
| `docs/`     | Documentation only                           | `docs/contributing-guide`        |
| `security/` | Security improvements (discuss in DMs first) | `security/csp-worker-src`        |

<a id="commit-messages"></a>

### Commit messages

Keep them short and imperative:

```
Fix export producing HTML instead of blob data
Add keyboard shortcut for container lock
Refactor VFS orphan detection to O(n) pass
```

No issue numbers in the subject line — put those in the PR description instead. No `WIP:` commits in the final branch.

<a id="pr-checklist"></a>

### Pull request checklist

Before marking the PR as ready for review:

-   [ ] Tested in at least one supported browser
-   [ ] No `console.log` or debug artifacts left in the code
-   [ ] No new external dependencies introduced
-   [ ] Existing behavior is not broken for cases you didn't touch
-   [ ] If you changed `daemon.js` — read [Security contribution rules](#security-rules) first
-   [ ] PR description explains **what** changed and **why**, not just **how**

---

<a id="code-style"></a>

## 🎨 Code style

<a id="style-general"></a>

### General rules

-   **Match the style of the file you're editing.** Indentation, spacing, quote style, comment language — all of it. Don't mix styles within a file
-   **No unnecessary abstractions.** Don't create a helper for something used once. Don't design for hypothetical future requirements
-   **Comments explain _why_, not _what_.** If the code is obvious, don't comment it. If it isn't obvious, explain the reasoning — not the mechanics
-   **No dead code.** Don't comment out unused blocks and leave them — delete them

<a id="style-js"></a>

### JavaScript specifics

The codebase is vanilla ES2020+ JavaScript — no frameworks, no TypeScript. A few conventions to follow:

-   Use `const` for everything that doesn't need reassignment, `let` otherwise. No `var`
-   Prefer early returns over deep nesting
-   Async functions use `async/await` — no raw `.then()` chains unless combining with `Promise.allSettled` or similar
-   String concatenation uses template literals `` `${x}` `` for readability; the concatenation operator `'' + x` is reserved for places where `String()` calls must be avoided for security reasons (see `daemon.js` for context)
-   `for` loops with index variables for performance-critical paths; `for...of` for readability in non-critical paths
-   Group related declarations on one line when they are semantically linked:
    ```js
    // Good — same logical unit
    let offset = 0,
        count = 0,
        valid = true;
    ```

<a id="style-html-css"></a>

### HTML & CSS

-   HTML attributes stay on one line unless there are more than ~4 and readability suffers
-   CSS follows the existing class naming — BEM is not enforced, but names should be descriptive and scoped to their component
-   No inline styles in HTML except where dynamic values make them unavoidable (e.g. `style="left: ${x}px"`)
-   No `!important` except where intentional override is the documented purpose (e.g. lockdown veil)

---

<a id="security-rules"></a>

## 🔐 Security contribution rules

SafeNova handles **encrypted data and derived cryptographic keys in a live browser environment**. This makes security changes fundamentally different from normal feature work.

**If your change touches any of the following, open a discussion issue or contact the maintainer before writing code:**

-   `daemon.js` — the Proactive anti-tamper runtime guard
-   `crypto.js` — AES-256-GCM + Argon2id layer
-   `state.js` — session key storage and three-source key wrapping
-   `db.js` — IndexedDB abstraction (container and file record layout)
-   The Content Security Policy in `index.html`
-   Any change that relaxes an existing restriction (e.g. whitelisting a new URL scheme, removing a hook)

> **Why the extra step?** Security changes that look correct can introduce subtle regressions. The Proactive guard in particular has carefully documented reasons for every design decision — a change that seems like a simplification may silently remove a specific defense. Discussing first prevents a PR that cannot be merged from wasting your time.

**Responsible disclosure for vulnerabilities:** If you find a security vulnerability (bypass of the Proactive guard, key material leakage, CSP bypass, etc.), please **do not file a public issue**. Contact the maintainer directly through GitHub. You will get credit in the changelog.

---

<a id="not-accepted"></a>

## 🚫 What we do NOT accept

To save everyone's time — PRs in the following categories will be closed without merge:

| Category                           | Reason                                                                                                  |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
| External runtime dependencies      | SafeNova has zero external dependencies by design. Adding `npm` packages is a non-starter               |
| Framework migrations               | React, Vue, Svelte, etc. — no. The codebase is intentionally framework-free                             |
| TypeScript conversion              | Not planned.                                                                                            |
| Weakened security controls         | Any change that removes or relaxes an existing Proactive check, CSP directive, or encryption constraint |
| UI cosmetic overhauls              | Minor tweaks are fine; wholesale redesigns need prior discussion                                        |
| Localization / i18n infrastructure | Out of scope for the current version                                                                    |

---

If you're unsure whether your idea fits — just open an issue and ask. It's faster than writing code that doesn't land.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023-2026 DosX

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<img src="./pics/intro.png" style="display: block; margin: 0 auto; max-width:80%; max-height:80%; border-radius:8px; margin-bottom:16px">

> ### Try it online: [https://safenova.dosx.su/](https://safenova.dosx.su/)

<a id="what-it-is"></a>

## ❔ What it is

SafeNova is a single-page web app that lets you create encrypted **containers** — isolated vaults where you can organize files in a folder structure, much like a regular desktop file manager. Everything is encrypted client-side before being written to storage. Nothing ever leaves your device.

![](./pics/screenshot.png)

Key properties:

-   **Zero-knowledge** — the app never sees your password or plaintext data
-   **Offline-first** — works entirely without network access
-   **No installation** — start the local server and you're running (or use online)

---

## 📚 Table of Contents

-   [❔ What it is](#what-it-is)
-   [🚀 Getting started](#getting-started)
    -   [Option A — Use online version](#getting-started-online)
    -   [Option B — Local server](#getting-started-local)
-   [📋 Requirements](#requirements)
-   [⚙️ Features](#features)
-   [⚔️ SafeNova vs. the Competition](#comparison)
-   [📁 Project structure](#project-structure)
-   [🔒 How containers work](#how-containers-work)
-   [📄 The `.safenova` Container Format](#container-format)
    -   [Archive sections](#container-format-archive-sections)
    -   [Design properties](#container-format-design-properties)
-   [🔐 Encryption](#encryption)
    -   [Session token security](#session-token-security)
    -   [Current tab session](#current-tab-session)
    -   [Stay signed in](#stay-signed-in)
    -   [Three-source key wrapping](#three-source-key-wrapping)
    -   [Session payload format](#session-payload-format)
    -   [Remaining trade-off](#remaining-trade-off)
-   [🔏 Content Security Policy](#content-security-policy)
    -   [Meta tag](#csp-meta-tag)
    -   [Server-level headers](#csp-server-headers)
-   [🛡️ Cross-Tab Session Protection](#cross-tab-session-protection)
-   [🛑 Duress Password](#duress-password)
    -   [How it works](#duress-how-it-works)
    -   [Why this design](#duress-why-this-design)
    -   [Technical details](#duress-technical-details)
-   [🔬 SafeNova Proactive Anti-Tamper](#safenova-proactive-antitamper)
    -   [Startup sequence](#proactive-startup-sequence)
        -   [Why native restoration matters](#proactive-native-restoration-advantage)
    -   [Real-time watchdog](#proactive-watchdog)
    -   [Watchdog resilience](#proactive-watchdog-resilience)
    -   [Intentionally excluded from checks](#proactive-excluded-checks)
    -   [Network request interception](#proactive-network-interception)
    -   [DOM exfiltration defense](#proactive-dom-exfiltration)
    -   [Threat response](#proactive-threat-response)
    -   [Design philosophy](#proactive-design-philosophy)
        -   [Hook opacity](#proactive-hook-opacity)
-   [🔍 Container Integrity Scanner](#container-integrity-scanner)
    -   [Phase 1 — VFS structural checks](#scanner-phase-1)
    -   [Phase 2 — Database-level checks](#scanner-phase-2)
-   [⚡ Performance](#performance)
    -   [Adaptive concurrency](#adaptive-concurrency)
    -   [Bulk upload](#bulk-upload)
    -   [ZIP export](#zip-export)
    -   [Password change](#password-change)
    -   [Container export](#container-export)
    -   [Drag-and-drop performance](#drag-drop-performance)
-   [📱 Mobile Touch Support](#mobile-touch-support)
    -   [Long-press to drag](#mobile-long-press)
    -   [Multi-file drag](#mobile-multi-file-drag)
    -   [Context menu](#mobile-context-menu)
    -   [Paste at finger position](#mobile-paste-at-finger-position)
    -   [Overscroll](#mobile-overscroll)
-   [�️ Security Audit Changelog](#security-audit)
-   [�🛠️ Contribute](#contribute)
-   [💬 Community](#community)
-   [🤝 Thanks to all contributors](#thanks)

---

<a id="getting-started"></a>

## 🚀 Getting started

<a id="getting-started-online"></a>

### Option A — Use online version

SafeNova is hosted on: [https://safenova.dosx.su/](https://safenova.dosx.su/)

<a id="getting-started-local"></a>

### Option B — Local server

A zero-dependency PowerShell server is included:

```powershell
.\\.server.ps1
```

Or right-click the file → **Run with PowerShell**. It starts an HTTP server on port `7777` (or the next free port) and opens the app in your default browser.

No external installs needed — it uses the Windows built-in `HttpListener`.

---

<a id="requirements"></a>

## 📋 Requirements

-   A modern browser: **Chrome 90+**, **Firefox 90+**, **Safari 15+**, or **Edge 90+**
-   Web Crypto API must be available — this requires either **HTTPS** or **`localhost`**
-   No plugins, no extensions, no backend

---

<a id="features"></a>

## ⚙️ Features

-   **Multiple containers** — each with its own password and independent storage limit (8 GB per container)
-   **Virtual filesystem** — nested folders, drag-to-reorder icons, customizable folder colors
-   **File operations** — upload (drag & drop or browse; folder upload with 4× parallel encryption), download, copy, cut, paste, rename, delete
-   **Built-in viewers** — text editor, image viewer, audio/video player, PDF viewer
-   **Hardware key support** — optionally use a WebAuthn passkey to strengthen the container salt
-   **Session memory** — optionally remember your session per tab (ephemeral, recommended) or persistently until manually signed out, using AES-GCM-encrypted session tokens; persistent sessions survive browser restarts
-   **Cross-tab session protection** — a container can only be actively open in one browser tab at a time; a lightweight lock protocol detects conflicts and offers instant session takeover
-   **Container import / export** — portable `.safenova` container files; import reads the archive via streaming `File.slice()` without loading the full file into memory, making multi-gigabyte imports possible; export streams data chunk-by-chunk requiring no single contiguous allocation regardless of container size
-   **Export password guard** — configurable setting (on by default) to require password confirmation before exporting; when disabled, the container key is taken directly from the active session if one is open; if no session is present, a pre-generated encrypted export cache stored in IDB is used — the cache payload is deflate-compressed before encryption, reducing its IDB footprint significantly for containers with many files; the compressed bytes are then wrapped with a per-container HKDF-SHA-256 derived key (AES-256-GCM), making the cache browser-independent; if the cache is absent or stale (file count or sizes changed), the context menu shows a red dot and falls back to a password prompt — after a successful password-prompted export the cache is rebuilt automatically so subsequent exports require no password; the cache is invalidated on password change or settings re-enable
-   **Quick export button** — dedicated **Export** button in the desktop toolbar provides one-click passwordless export when the export password guard is disabled
-   **Sort & arrange** — sort icons by name, date, size, or type; drag to custom positions
-   **Secure container deletion** — before permanent erasure, every encrypted blob is cryptographically pre-shredded: inline files have random bytes XOR-flipped (position and delta are unknown and unlogged); large chunked files have their AES-GCM IV zeroed, making decryption unconditionally impossible and the operation maximally fast; heavy internal blobs (deferred workspace data, export cache, audit log) are explicitly nullified before the record is deleted so that the browser immediately releases persistent storage and the freed space is reflected without waiting for lazy garbage collection
-   **Duress password** — optional panic password that, when entered anywhere (unlock, change password, export), looks exactly like an incorrect password but silently destroys all encrypted data in the background; see [Duress Password](#duress-password) below
-   **SafeNova Proactive** — runtime protection module that loads first in `<head>`, captures all security-critical native function references at startup (including `String.prototype.toLowerCase`, `String.prototype.indexOf`, and `String.prototype.slice` for tamper-proof string operations), validates every capture is truly native (pre-capture tampering guard), hooks outbound network APIs (fetch, XHR, sendBeacon, WebSocket, window.open, EventSource, Worker/SharedWorker — including `data:` and same-origin `blob:` workers) and DOM exfiltration vectors (setAttribute, innerHTML/outerHTML, insertAdjacentHTML, document.write, Location navigation, form submit, resource property setters) to block external requests, silently removes dynamically injected external scripts via MutationObserver, blocks `eval` and `new Function()` constructors, guards string callbacks in setTimeout/setInterval, and runs a quadruple-redundant watchdog with timer-ID protection and a dead man's switch heartbeat — if the watchdog is killed, the app auto-locks all containers
-   **Container integrity scanner** — 28 automated checks (21 VFS structural + 7 database-level) with one-click auto-repair, **Deep Clean** (flattens over-nested folder trees, repairs all metadata), and a backup prompt before any destructive operation; includes file decryption verification that detects corrupted or unreadable blobs (including those silently destroyed by the duress trigger)
-   **Settings** — three tabs: personalization, statistics, activity logs
-   **Keyboard shortcuts** — `Delete`, `F2`, `Ctrl+A`, `Ctrl+C/X/V`, `Ctrl+S` (save in editor), `Escape`, `End` (lock container — only when focus is not in a text field)
-   **Incognito / private-mode detection** — on first visit the app detects if the browser is in private/incognito mode (Chrome, Firefox, Safari) using engine-fingerprint-based checks (no UA sniffing). If detected, a one-time warning explains that IndexedDB is ephemeral in private mode and encrypted containers will be lost when the tab is closed; the user can acknowledge and continue normally, or switch to a regular browser window first
-   **Mobile-friendly** — long-press to drag icons, rubber-band selection, single/double-tap gestures, paste at finger position, multi-file drag with per-item snap previews

---

<a id="comparison"></a>

## ⚔️ SafeNova vs. the Competition

We think SafeNova has real strengths worth knowing about — but every tool has its place. Compare for yourself and pick what fits your use case.

Legend: ✅ Advantage / works well &nbsp;·&nbsp; ❌ Disadvantage / not supported &nbsp;·&nbsp; 🟡 Partial / situational

<table>
<thead>
<tr>
<th align="left">Feature</th>
<th align="left"><a href="https://safenova.dosx.su/">SafeNova</a></th>
<th align="left"><a href="https://www.veracrypt.fr/">VeraCrypt</a></th>
<th align="left"><a href="https://learn.microsoft.com/windows/security/operating-system-security/data-protection/bitlocker/">BitLocker</a></th>
<th align="left"><a href="https://cryptomator.org/">Cryptomator</a></th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><b>Best suited for</b></td>
<td align="left">Personal files on shared or managed machines — zero-install, browser-only, no disk traces</td>
<td align="left">Large encrypted volumes on own hardware; plausible deniability</td>
<td align="left">IT-managed Windows with full-disk encryption and central key recovery</td>
<td align="left">Encrypting files before syncing to cloud (Dropbox, Google Drive, OneDrive…)</td>
</tr>
<tr>
<td align="left"><b>Cross-platform</b></td>
<td align="left">✅ Any browser — Windows, macOS, Linux, Android, iOS</td>
<td align="left">🟡 Desktop only — Windows, macOS, Linux</td>
<td align="left">❌ Windows only</td>
<td align="left">✅ Windows, macOS, Linux, Android, iOS</td>
</tr>
<tr>
<td align="left"><b>No installation</b></td>
<td align="left">✅ Zero install, runs in the browser</td>
<td align="left">❌ Requires system installation</td>
<td align="left">❌ Windows Pro/Enterprise only</td>
<td align="left">❌ Requires a desktop or mobile app</td>
</tr>
<tr>
<td align="left"><b>Admin / root rights</b></td>
<td align="left">✅ None required</td>
<td align="left">❌ Required for mounting</td>
<td align="left">❌ Required</td>
<td align="left">🟡 None on Windows/iOS; macOS needs macFUSE; Linux needs FUSE</td>
</tr>
<tr>
<td align="left"><b>Encryption algorithm</b></td>
<td align="left">✅ AES-256-GCM — authenticated encryption; every ciphertext has an integrity tag</td>
<td align="left">✅ AES / Twofish / Serpent (configurable)</td>
<td align="left">🟡 AES-128/256 XTS — no authentication tag</td>
<td align="left">✅ AES-256-GCM per file</td>
</tr>
<tr>
<td align="left"><b>Key derivation</b></td>
<td align="left">✅ Argon2id — memory-hard; GPU brute-force is expensive</td>
<td align="left">🟡 PBKDF2-SHA-512 / Whirlpool — not memory-hard; GPU-crackable</td>
<td align="left">🟡 TPM-bound; password KDF is comparatively weak</td>
<td align="left">✅ scrypt — memory-hard; comparable to Argon2id</td>
</tr>
<tr>
<td align="left"><b>Per-item authentication</b></td>
<td align="left">✅ GCM tag per chunk — tampering always detected</td>
<td align="left">❌ Block-level only; no per-file MAC</td>
<td align="left">❌ XTS provides no authentication</td>
<td align="left">✅ GCM tag per file</td>
</tr>
<tr>
<td align="left"><b>Portable container</b></td>
<td align="left">✅ Single <code>.safenova</code> file — copy anywhere, open anywhere</td>
<td align="left">🟡 Single container file, but fixed pre-allocated size</td>
<td align="left">❌ Tied to the Windows NTFS partition</td>
<td align="left">🟡 Folder of encrypted files — portable, but not a single archive</td>
</tr>
<tr>
<td align="left"><b>File stealer protection</b></td>
<td align="left">✅ Encrypted in IDB; never plaintext on disk</td>
<td align="left">❌ Mounted volume exposes all files to every process</td>
<td align="left">❌ Once unlocked, all files accessible to all processes</td>
<td align="left">🟡 Encrypted on disk; plaintext only in the virtual drive while open</td>
</tr>
<tr>
<td align="left"><b>Session / key management</b></td>
<td align="left">✅ Three-source HKDF wrap key; tab + browser sessions; cross-tab invalidation</td>
<td align="left">❌ Key in RAM while mounted; no session concept</td>
<td align="left">❌ TPM-derived at boot; no session control</td>
<td align="left">❌ Key in memory while open; no session tokens or expiry</td>
</tr>
<tr>
<td align="left"><b>Duress / emergency wipe</b></td>
<td align="left">✅ Duress password silently destroys the container</td>
<td align="left">❌ Not supported</td>
<td align="left">❌ Not supported</td>
<td align="left">❌ Not supported</td>
</tr>
<tr>
<td align="left"><b>Runtime anti-tamper</b></td>
<td align="left">✅ SafeNova Proactive — native API restoration, 20+ hooks, quadruple watchdog</td>
<td align="left">🟡 N/A — native binary; no browser JS attack surface</td>
<td align="left">🟡 N/A — same</td>
<td align="left">🟡 N/A — same</td>
</tr>
<tr>
<td align="left"><b>Content Security Policy</b></td>
<td align="left">✅ Strict CSP (meta tag + server headers); blocks inline scripts and external loads</td>
<td align="left">🟡 N/A — browser mechanism; not applicable to native apps</td>
<td align="left">🟡 N/A — same</td>
<td align="left">🟡 N/A — same</td>
</tr>
<tr>
<td align="left"><b>Integrity scanner</b></td>
<td align="left">✅ 28 automated checks (VFS + DB); auto-repair; decryption verification</td>
<td align="left">❌ No built-in scanning</td>
<td align="left">❌ No per-file integrity</td>
<td align="left">🟡 Detects corrupt files; no automated repair</td>
</tr>
<tr>
<td align="left"><b>Export / backup</b></td>
<td align="left">✅ One-click export as <code>.safenova</code> or ZIP</td>
<td align="left">🟡 Container file is portable but fixed size; no incremental backup</td>
<td align="left">❌ Cannot export; tied to the Windows volume</td>
<td align="left">✅ Files sync individually — cloud acts as continuous backup</td>
</tr>
<tr>
<td align="left"><b>Data deletion</b></td>
<td align="left">✅ Blob shredding + full IDB purge on delete</td>
<td align="left">🟡 Delete the file; OS journaling may retain fragments</td>
<td align="left">❌ Decryption leaves files; separate secure-erase needed</td>
<td align="left">🟡 Delete the vault; journaling applies; cloud may retain versions</td>
</tr>
<tr>
<td align="left"><b>Code auditability</b></td>
<td align="left">✅ Open source; plain JS; no build pipeline</td>
<td align="left">✅ Open source; multiple independent audits</td>
<td align="left">❌ Closed source; no audit possible</td>
<td align="left">✅ Open source; independent audits conducted</td>
</tr>
<tr>
<td align="left"><b>Performance at scale</b></td>
<td align="left">🟡 Good for typical files; slower than native for bulk operations</td>
<td align="left">✅ Native + AES-NI; minimal overhead</td>
<td align="left">✅ Kernel driver + AES-NI; transparent to the OS</td>
<td align="left">✅ Native; per-file overhead is minimal; handles large libraries</td>
</tr>
<tr>
<td align="left"><b>Targeted attack protection</b></td>
<td align="left">🟡 Blocks JS injection; limited against full-OS compromise</td>
<td align="left">🟡 Anti-forensic; cannot stop OS-level keyloggers</td>
<td align="left">❌ TPM bus sniffing (Evil Maid) is a known vector</td>
<td align="left">🟡 No special runtime protection; same OS-level limits</td>
</tr>
<tr>
<td align="left"><b>Storage size</b></td>
<td align="left">❌ Max 8 GB per container; IDB quota applies; not for large or industrial-scale data</td>
<td align="left">✅ Disk-only limit; terabyte-scale supported</td>
<td align="left">✅ Full drive at any capacity</td>
<td align="left">✅ No built-in limit; disk / cloud quota only</td>
</tr>
<tr>
<td align="left"><b>Hidden volumes</b></td>
<td align="left">❌ Not supported</td>
<td align="left">✅ Hidden volumes + hidden OS partition</td>
<td align="left">❌ Not supported</td>
<td align="left">❌ Not supported</td>
</tr>
<tr>
<td align="left"><b>OS / filesystem integration</b></td>
<td align="left">❌ Browser sandbox only; no virtual drive mount</td>
<td align="left">✅ Mounts as a real drive letter; full shell integration</td>
<td align="left">✅ Transparent OS encryption; Group Policy; BitLocker To Go</td>
<td align="left">✅ Mounts as a virtual drive (WebDAV / FUSE)</td>
</tr>
<tr>
<td align="left"><b>Multi-user access</b></td>
<td align="left">❌ Single user per container</td>
<td align="left">❌ Single user at a time</td>
<td align="left">🟡 Multiple recovery keys; enterprise AD deployment</td>
<td align="left">❌ Single shared password; per-user control requires Cryptomator Hub (separate server)</td>
</tr>
</tbody>
</table>

---

<a id="project-structure"></a>

## 📁 Project structure

```
SafeNova/
│
├── index.html          # Single-page app entry point
├── favicon.png         # Application icon
├── .server.ps1         # Local PowerShell dev server (Windows)
│
├── css/
│   └── app.css         # All application styles
│
└── js/
    ├── proactive/
    │   └── daemon.js          # SafeNova Proactive — anti-tamper runtime integrity guard (loads first of all)
    ├── libs/
    │   └── argon2.umd.min.js  # Argon2id WASM/JS implementation (hashwasm)
    ├── detectors/
    │   └── incognito.js       # Incognito / private-mode detector — warns on first visit about limitations and risks
    ├── docmode.js             # Pre-CSS docmode guard (runs before stylesheet loads)
    ├── initlog.js             # Initialization stage console logger (InitLog)
    ├── constants.js           # Shared constants (IDB names, limits, chunk size), utilities, icon SVGs, duress hash helpers
    ├── db.js                  # IDB abstraction — SafeNovaEFS (containers / files / vfs / chunks stores)
    ├── crypto.js              # AES-256-GCM + Argon2id encryption layer
    ├── vfs.js                 # In-memory virtual filesystem (nodes, positions, child index)
    ├── state.js               # App state singleton — key, session encrypt/decrypt, three-source wrap key
    ├── home.js                # Container management: create, unlock, import, export, change password
    ├── desktop.js             # Desktop UI: icons, folder windows, drag & drop, integrity scanner
    ├── fileops.js             # File operations: upload, download, open, copy/paste, rename, delete, ZIP export; export cache management for passwordless export
    └── main.js                # App boot, event binding, console security warning
```

---

<a id="how-containers-work"></a>

## 🔒 How containers work

1. **Create** a container with a name and password
2. **Unlock** the container — Argon2id derives the key from your password
3. Files you upload are encrypted with AES-256-GCM before being saved to IDB
4. The virtual filesystem (folder tree + icon positions) is also encrypted and saved separately
5. **Lock** the container — the derived container key is wiped from memory; if the **Export password guard** setting is disabled, a pre-generated export cache (built when the setting was disabled and kept up to date after every file operation) remains in the database, ready for the next passwordless export
6. **Delete** the container — first, every encrypted blob is cryptographically pre-shredded (random bytes XOR-flipped for inline files; IV zeroed for large chunked files); then heavy internal blobs (deferred workspace data, export cache, audit log) are nullified to force immediate browser-level storage release; finally all encrypted records, the VFS blob, and the container metadata are permanently deleted from IDB

All container data is scoped to the current browser and device. Use **Export Container** to back up or transfer to another device.

---

<a id="container-format"></a>

## 📄 The `.safenova` Container Format

Exported containers are saved as `.safenova` files. This is a **self-contained structured archive** with a versioned, deterministic layout. It is designed so that no file content or filesystem metadata is ever present in plaintext within the archive.

<a id="container-format-archive-sections"></a>

### Archive sections

| Section                      | Role                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `container.xml`              | Plaintext container manifest: name, creation timestamp, Argon2id salt, and the AES-GCM verification IV and blob needed to authenticate a password at import. No file names or content appear here                                                                                                                                                    |
| `meta/0`                     | The IV (initialization vector) used to encrypt the VFS blob                                                                                                                                                                                                                                                                                          |
| `meta/1`                     | The encrypted VFS blob — the complete folder hierarchy, file names, MIME types, sizes, timestamps, icon positions, and folder colors, all ciphertext                                                                                                                                                                                                 |
| `meta/2`                     | The IV for the encrypted file manifest                                                                                                                                                                                                                                                                                                               |
| `meta/3`                     | The encrypted file manifest — a JSON structure mapping each file's internal ID to its byte offset and length within `workspace.bin`, encrypted with the container key. When the export password guard is disabled, this blob is taken directly from the pre-built export cache stored in the container record, avoiding re-encryption at export time |
| `safenova_efs/workspace.bin` | A single contiguous block of raw ciphertext — the encrypted content of every file, concatenated end-to-end. Without the decryption key, file boundaries and content are indistinguishable                                                                                                                                                            |
| `meta/activity_logs/0`       | _(Optional)_ The encrypted activity log, included only when the `exportWithLogs` container setting is enabled                                                                                                                                                                                                                                        |

<a id="container-format-design-properties"></a>

### Design properties

#### Zero plaintext leakage

The only identifiable plaintext in the archive is the container name in `container.xml` and the Argon2id salt. All file names, folder structure, and content are ciphertext.

#### Lazy import

A `.safenova` file can be imported without entering the container password. The archive is parsed via streaming slicing — only the directory and small metadata entries are fully read into memory; the `workspace.bin` payload is handled as a lightweight reference. The encrypted workspace is stored as-is internally and flagged as a deferred workspace. It is expanded into the local database only on first unlock — so import is instantaneous regardless of container size.

#### Self-authenticating

The salt and verification blob in `container.xml` allow the application to confirm the correctness of a supplied password before touching any file data, preventing unnecessary decryption work.

#### Versioned

The `version` attribute in the XML manifest distinguishes between format generations, enabling forward-compatible import logic. Currently only version 3 is supported; earlier formats have been retired.

---

<a id="encryption"></a>

## 🔐 Encryption

| Layer            | Algorithm                                              |
| ---------------- | ------------------------------------------------------ |
| Key derivation   | Argon2id (19 MB memory, 2 iterations, 1 thread)        |
| File encryption  | AES-256-GCM (random 96-bit IV per file)                |
| VFS encryption   | AES-256-GCM (same key, independent IV)                 |
| Session tokens   | AES-256-GCM, dual-key: per-tab ephemeral or persistent |
| Browser key wrap | HKDF-SHA-256 from fingerprint + cookie + IDB           |
| Integrity check  | AES-256-GCM verification blob authenticated on open    |
| Duress hash      | SHA-256(random 32-byte salt ‖ password), IDB-only      |

Every file is encrypted individually — each with its own freshly generated IV. The virtual filesystem (folder tree, file names, sizes, positions) is encrypted as a separate blob using the same derived key. The plaintext password is never stored; only the derived key is held in JavaScript memory for the duration of an active session.

File keys are derived from passwords through **Argon2id** with OWASP-recommended minimum parameters (19 MB memory cost, 2 iterations), providing strong resistance against brute-force and GPU-accelerated attacks.

<a id="session-token-security"></a>

### Session token security

SafeNova uses a **dual-key model** for session storage — an ephemeral per-tab key and a persistent shared key — each scoped to a distinct user intent.

<a id="current-tab-session"></a>

#### Current tab session _(Recommended)_

The 32-byte Argon2id key material is encrypted with **`snv-sk`** — a per-tab AES-256-GCM key stored in `sessionStorage`. `snv-sk` is itself wrap-encrypted with the same three-source HKDF key as `snv-bsk` before being written to `sessionStorage`. This means:

-   The session blob (`snv-s-{cid}`) lives in `sessionStorage` and is readable only by the exact tab that created it
-   Closing the tab permanently destroys `snv-sk` — no residue remains in any persistent storage
-   An attacker with access to `localStorage`, `sessionStorage`, or disk snapshots gains nothing — even a raw `sessionStorage` dump does not expose the decryption key without also possessing the browser fingerprint, the `snv-kc` cookie, and the `SafeNovaKS` IDB record

This is the recommended option: the session is automatically gone as soon as the tab is closed.

<a id="stay-signed-in"></a>

#### Stay signed in

The key material is encrypted with **`snv-bsk`** — a shared AES-256-GCM key available to all tabs of the same browser origin.

<a id="three-source-key-wrapping"></a>

#### Three-source key wrapping

Before `snv-bsk` is written to `localStorage`, it is itself encrypted with a separate _wrap key_ that is derived on-the-fly via **HKDF-SHA-256** from **three independent sources** and **never stored anywhere**:

| #   | Source              | Storage                                       | Purpose                                                                                          |
| --- | ------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 1   | Browser fingerprint | _(computed)_                                  | `origin \0 userAgent \0 platform \0 language \0 hardwareConcurrency \0 colorDepth \0 pixelDepth` |
| 2   | `snv-kc` cookie     | Cookie jar (`SameSite=Strict`, ~400 days TTL) | 32 random bytes, isolated from localStorage                                                      |
| 3   | `snv-ki` record     | Separate IDB `SafeNovaKS`                     | 32 random bytes, independent from main `SafeNovaEFS` database                                    |

```
ikm      = fingerprint \0 cookie_bytes(32) \0 idb_bytes(32)
wrap_key = HKDF-SHA-256( ikm, salt=0×32, info="snv-browser-wrap-v3" )
snv-bsk (localStorage)   = IV(12) || AES-256-GCM( wrap_key, raw_bsk_bytes )
snv-sk  (sessionStorage) = IV(12) || AES-256-GCM( wrap_key, raw_sk_bytes  )
```

Consequences:

-   Any tab in the **same browser** recomputes the identical fingerprint, reads the same cookie and IDB secret → identical wrap key → can decrypt `snv-bsk` and resume the session seamlessly
-   An attacker must compromise **all three storage mechanisms** simultaneously to reconstruct the wrap key — `localStorage` alone, a disk image, or a partial export will not suffice:
    -   Copying `localStorage` without the cookie and `SafeNovaKS` database → wrap key cannot be derived → `snv-bsk` is opaque
    -   Clearing cookies invalidates the cookie component → sessions become undecryptable
    -   Deleting or moving the `SafeNovaKS` database invalidates the IDB component → same effect
-   The fingerprint includes `navigator.userAgent` and `navigator.platform`, binding sessions to the specific browser version and OS. **Browser updates that change the UA string will invalidate existing sessions** — the user re-enters their password once and a new session is established automatically
-   If any of the three components change (fingerprint shift, cookie clearing, IDB loss), the stored `snv-bsk` can no longer be decrypted; a new key is generated automatically and the user must re-enter the password once — any `snv-sb-{cid}` blobs encrypted with the old key are silently dropped
-   **Legacy format migration:** `snv-bsk` and `snv-sk` entries written before wrap-encryption was introduced (raw 32-byte keys, no IV prefix) are detected by their exact byte length and silently re-wrapped in the current `IV(12) || AES-GCM` format on first access — no user action required
-   The session expires after **7 days** (TTL baked into the encrypted payload), or immediately on explicit sign-out

<a id="session-payload-format"></a>

#### Session payload format

Both scope types use the same blob layout: `IV(12) || AES-256-GCM(scope_key, expiry(8 bytes, uint64 LE) || raw_key(32 bytes))`. The AES-GCM call is authenticated with the container ID as additional data (`snv-session:{cid}`), preventing a blob from one container from being replayed to unlock a different container. Tab-scope sessions use `expiry = Number.MAX_SAFE_INTEGER` (no TTL — the tab's `sessionStorage` is the only lifetime bound); browser-scope sessions carry a hard 7-day expiry.

<a id="remaining-trade-off"></a>

#### Remaining trade-off

An attacker with live access to the running browser process (e.g. malicious extension, XSS) can still call the same fingerprint function, read the cookie, and query the `SafeNovaKS` IDB to derive the wrap key. The three-source wrapping layer protects against _offline_ credential theft (disk images, direct `localStorage` dumps, partial storage exports), not against in-browser code execution.

---

<a id="content-security-policy"></a>

## 🔒 Content Security Policy

<a id="csp-meta-tag"></a>

### Meta tag (inline)

`index.html` declares a strict per-directive CSP via `<meta http-equiv="Content-Security-Policy">`:

| Directive     | Value                       |
| ------------- | --------------------------- |
| `default-src` | `'none'`                    |
| `script-src`  | `'self' 'wasm-unsafe-eval'` |
| `style-src`   | `'self' 'unsafe-inline'`    |
| `img-src`     | `'self' blob: data:`        |
| `media-src`   | `blob:`                     |
| `frame-src`   | `blob: about:`              |
| `font-src`    | `'self'`                    |
| `connect-src` | `'self'`                    |
| `worker-src`  | `'self' blob:`              |
| `base-uri`    | `'self'`                    |
| `form-action` | `'none'`                    |
| `object-src`  | `'none'`                    |

`'unsafe-inline'` is absent from `script-src`. There are no inline `<script>` blocks — all JavaScript is loaded as external files via `'self'`. Argon2id WASM compilation is permitted by `'wasm-unsafe-eval'`. `about:` is added to `frame-src` to allow **SafeNova Proactive** to create a temporary hidden iframe at startup for capturing pristine, extension-untampered native references (the iframe is removed from the DOM immediately after capture).

<a id="csp-server-headers"></a>

### Server-level headers (`.server.ps1`)

When running via the included PowerShell dev server, every response additionally carries:

| Header                         | Value                                                          |
| ------------------------------ | -------------------------------------------------------------- |
| `X-Content-Type-Options`       | `nosniff`                                                      |
| `X-Frame-Options`              | `DENY`                                                         |
| `Referrer-Policy`              | `no-referrer`                                                  |
| `Permissions-Policy`           | `interest-cohort=(), geolocation=(), camera=(), microphone=()` |
| `Cross-Origin-Opener-Policy`   | `same-origin`                                                  |
| `Cross-Origin-Embedder-Policy` | `require-corp`                                                 |

`Cross-Origin-Opener-Policy: same-origin` prevents other origins from holding a reference to the app window. `Cross-Origin-Embedder-Policy: require-corp` blocks cross-origin subresource loads that lack explicit CORP headers — irrelevant in practice since all resources are same-origin, but also a prerequisite for enabling `SharedArrayBuffer` if needed in the future.

---

<a id="cross-tab-session-protection"></a>

## 🛡️ Cross-Tab Session Protection

To prevent a container from being open in two browser tabs simultaneously — which would risk conflicting VFS writes — SafeNova maintains a lightweight **session lock** in `localStorage`.

When a container is unlocked, the tab writes a claim entry (`snv-open-{id}`) containing its unique tab identifier and a timestamp. A **heartbeat** refreshes the timestamp every 5 seconds. Any other tab that reads a live claim (timestamp within the 30-second TTL) before opening the same container is shown a conflict dialog offering to take over the session.

On accepting the takeover, the requesting tab writes a **kick flag** into the claim entry. The original tab listens for `storage` events on this key and immediately locks itself when the flag is detected. On normal tab close, `beforeunload` and `pagehide` remove the claim entry so the container becomes available to other tabs without waiting for the TTL to expire.

---

<a id="duress-password"></a>

## 🛑 Duress Password

The duress password is a secondary password you can set for any container. It is designed for situations where you are forced to provide your password under coercion.

<a id="duress-how-it-works"></a>

### How it works

1. You set a duress password in **Settings → Danger Zone** (it must differ from your main password)
2. When the duress password is entered **anywhere** — the unlock screen, the change password dialog, or the export password prompt — the app responds with the standard **"Incorrect password"** error, exactly the same as any wrong password
3. Behind the scenes, every encrypted file blob in the container is silently and irreversibly corrupted
4. The duress hash and export cache are erased from the database, leaving no trace that a duress password ever existed
5. Later, when the real password is entered, the container opens normally — the folder tree and file names are intact — but every file is unreadable, indistinguishable from natural storage corruption

<a id="duress-why-this-design"></a>

### Why this design

An attacker watching over your shoulder sees exactly what they’d see with any wrong password — an error message. There is no special screen, no empty vault, nothing that reveals a duress mechanism exists at all. The destruction is invisible and happens before the “incorrect” error is shown.

Because the real password still works, you can unlock the container afterward to confirm the damage. The built-in **integrity scanner** will detect that files cannot be decrypted and can clean up the broken entries.

<a id="duress-technical-details"></a>

### Technical details

-   The duress hash is stored in IDB as a salted SHA-256 hash — never exported to `.safenova` files
-   Corruption method: random bytes are XOR-flipped with a random non-zero value in each encrypted blob (inline files). For large chunked files, the AES-GCM IV stored in the file record is zeroed instead — no chunk data is read, making corruption of large files maximally fast. Position and XOR delta are unknown, unlogged, and unreproducible. Any byte change in AES-GCM ciphertext, or a zeroed IV, causes authentication failure for the entire file
-   After triggering, the duress hash and export cache are deleted from the container record — no forensic residue
-   It can be toggled on and off in Settings → Danger Zone
-   The duress password must be at least 4 characters and must differ from the main password

---

<a id="safenova-proactive-antitamper"></a>

## 🛡️ SafeNova Proactive Anti-Tamper

SafeNova Proactive is a self-contained **anti-tamper runtime integrity guard** that loads **before every other application script**. Its aggressive threat model is Self-XSS and malicious browser extensions (MV2 `document_start` content scripts, cosmetic-filter injections): both classes of attack require modifying the JavaScript runtime environment in a way that can be detected by capturing native references before any attacker code runs. The application refuses to start if the guard is absent or failed to initialize.

> ![](./pics/screenshot_proactive.png) **Silent by design.** Proactive runs entirely in the background with zero user-visible presence during normal operation. No indicators, no UI overlays, no interaction required — just quiet, constant verification of the cryptographic runtime underneath the application. Think of it as an immune system rather than antivirus: always active, completely invisible, and only surfaces when something genuinely suspicious is detected.

<a id="proactive-startup-sequence"></a>

### Startup sequence

1. **Earliest captures** — at the very first line of execution, before any other code runs, a set of core language primitives is captured into private constants that cannot be reassigned from outside: `Object.freeze`, `RegExp.prototype.test`, `Array.prototype.push`, `String.prototype.slice`, `String.prototype.toLowerCase`, and `String.prototype.indexOf`. Immediately after, three **pure operator-level string utilities** are built (`_pureToLower`, `_pureIndexOf`, `_pureSlice`) using only bracket indexing (`s[i]`), `.length`, `+` concatenation, and comparison operators — these have zero prototype method calls and are used for ALL string processing inside the daemon. The original captured references are retained solely for boot-time and per-tick native validation. If any of them were already replaced by a MV2 `document_start` extension, the structural boot check will detect it
2. **Native restoration via hidden iframe** — a temporary hidden `about:blank` iframe is created whose browsing context has never been touched by extensions (MV2 extensions only target the main window, not dynamically-created child frames). If the DOM primitives needed to create the iframe are verified as native, **50+ security-critical functions** are restored back to their pristine state on the main window from the iframe's untouched copies:
    - **Core language primitives** — `Object.defineProperty`, `Object.freeze`, `Reflect.apply`, `Reflect.construct`, and other foundational methods — restored first so all subsequent restoration steps use the native version
    - **Window globals** — `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `Worker`, `SharedWorker`, `MutationObserver`, `URL`, `Blob`, typed arrays, encoding/decoding, timers, `eval`, `Function`, compression streams
    - **Crypto** — `crypto.getRandomValues` and all 12 `SubtleCrypto` methods are restored individually (`window.crypto` itself is `[Unforgeable]` and cannot be replaced)
    - **Prototype methods** — XHR, EventTarget, Element, Node, Document, Storage, IDB, Form, Location, Navigator, typed array, and URL methods
    - **Console** — a tamper-proof console reference is captured from the iframe, immune to main-window replacement by extensions
    - The iframe is removed from the DOM immediately after; captured JS references survive removal
3. **Pre-existence check** — if a guard marker already exists on `window`, it means an attacker pre-defined it via `document_start` to fake the guard as active. This taints the boot
4. **Bootstrap validation** — `Function.prototype.toString` and `Function.prototype.call` are structurally validated (name, arity, and string coercion — using concatenation operators, not function calls) before building the capture registry. All regex tests in the bootstrap use captured `Reflect.apply`, so replacing `RegExp.prototype.test` at runtime cannot make fakes pass
5. **Structural validation of early captures** — the primitives captured in step 1 are validated by their structural properties (name, arity). This catches naive spoofing that forgets to replicate the original function's metadata
6. **Capture registry** — all security-critical native references are frozen into a single immutable object using the captured (not live) `Object.freeze`. From this point forward, the guard never calls live globals — only its own captured copies
7. **Pre-capture validation** — every captured reference is verified as truly native using indexed for-loops (not `for...of`, which depends on `Symbol.iterator`). If any capture is already non-native, the guard is tainted and the app refuses to boot
8. **Install protective hooks** — all hooks (network, DOM, timer, constructor, code-injection) are wrapped in an opaque forwarder. Calling `toString()` on any hooked function (e.g. `fetch.toString()` in the DevTools console) reveals only the forwarder — the actual security logic is unreachable from outside the closure. Every internal function invocation uses captured `Reflect.apply` instead of `.apply()` or `.call()`, making the entire daemon immune to `Function.prototype.apply` / `Function.prototype.call` replacement. DOM operations (overlay injection, element removal, attribute stripping, descendant scanning) use captured `Node.prototype.appendChild`, `Node.prototype.removeChild`, `Element.prototype.removeAttribute`, and `Element.prototype.querySelectorAll` references — not live prototype methods
9. Expose three non-configurable window properties:
    - A frozen boot-status token with a closure-private canary for cross-checks
    - A verification function that the application calls at startup to confirm the guard is genuine
    - An emergency lock function that directly wipes all session storage and in-memory state, bypassing the event system
10. Start the watchdog — **four independent timer mechanisms** running in parallel

<a id="proactive-native-restoration-advantage"></a>

#### Why native restoration matters

The iframe restoration in step 2 is the single most impactful defense in the entire startup sequence. Without it, any MV2 extension running at `document_start` — or a compromised CDN that injects a `<script>` above the guard — could replace `crypto.subtle.encrypt`, `fetch`, `Reflect.apply`, or any other global **before** the guard even begins to execute. A pure capture-and-validate approach can only detect such pre-load tampering after the fact and refuse to boot, but provides no recovery path.

Native restoration changes the equation: even if an attacker ran code _before_ the guard, the iframe's browsing context provides untouched native references directly from the browser engine. By restoring 50+ critical functions on the main window from these pristine copies, the guard **undoes pre-load replacements** — the attacker's hooks are overwritten _before_ anything is captured. This eliminates roughly **95% of function-replacement attack vectors** that would otherwise succeed against a capture-only design, reducing the viable pre-load attack surface to scenarios where the browser engine itself is compromised (which is outside the threat model of any userland JS defense).

<a id="proactive-watchdog"></a>

### Real-time watchdog

Each tick performs **five independent checks**:

**Hook integrity** — verifies by reference equality that the installed network hooks are still the exact functions placed by the guard. If any hook was removed or swapped by a third party (extensions like Adblock routinely wrap network APIs), the guard **silently re-installs** them without firing an alert — this is expected browser extension behaviour, not a security threat.

**Native function purity** — verifies that the following functions are still fully native using the captured `Function.prototype.toString` reference (immune to meta-spoofing). Any substitution fires an alert:

| Function                                                               | Purpose                                                                                                                                     |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `crypto.getRandomValues`                                               | IV / key generation                                                                                                                         |
| `crypto.subtle.{encrypt,decrypt,importKey,exportKey,deriveKey,digest}` | All cryptographic operations                                                                                                                |
| `IDBFactory.prototype.open`                                            | IDB access                                                                                                                                  |
| `Storage.prototype.{getItem,setItem,removeItem}`                       | Session key storage                                                                                                                         |
| `btoa` / `atob`                                                        | Base-64 encode/decode                                                                                                                       |
| `TextEncoder.prototype.encode` / `TextDecoder.prototype.decode`        | Text serialization / deserialization                                                                                                        |
| `Uint8Array`, `.prototype.{set,subarray,slice}`                        | Typed array integrity                                                                                                                       |
| `ArrayBuffer`, `.prototype.slice`                                      | Binary buffer integrity                                                                                                                     |
| `DataView`                                                             | Binary data views                                                                                                                           |
| `Blob`                                                                 | File blob construction                                                                                                                      |
| `URL`, `URL.createObjectURL` / `URL.revokeObjectURL`                   | URL parsing and blob URL lifecycle                                                                                                          |
| `CompressionStream` / `DecompressionStream` _(if available)_           | Compression pipeline integrity                                                                                                              |
| `Function.prototype.call` / `.apply`                                   | Meta-method hardening                                                                                                                       |
| `Reflect.apply`                                                        | Core of the native-check mechanism — must stay native                                                                                       |
| `EventTarget.prototype.addEventListener` / `.dispatchEvent`            | Event subscription and heartbeat                                                                                                            |
| `XMLHttpRequest.prototype.send`                                        | XHR send path hardening                                                                                                                     |
| `RegExp.prototype.test`                                                | Native-check regex — replacing with `()=>true` would bypass checks                                                                          |
| `Object.freeze`                                                        | Capture registry immutability                                                                                                               |
| `Array.prototype.push`                                                 | Captured for validation                                                                                                                     |
| `String.prototype.slice`                                               | Captured for boot-time + per-tick native validation only; actual string slicing uses `_pureSlice` (bracket + concatenation)                 |
| `String.prototype.toLowerCase`                                         | Captured for boot-time + per-tick native validation only; actual lowercasing uses `_pureToLower` (frozen A-Z→a-z lookup + bracket indexing) |
| `String.prototype.indexOf`                                             | Captured for boot-time + per-tick native validation only; actual substring search uses `_pureIndexOf` (nested indexed loop)                 |
| `Element.prototype.getAttribute`                                       | Used by MutationObserver defense layer to read attribute values safely                                                                      |
| `Element.prototype.removeAttribute`                                    | Used by MO observer and scanner to strip malicious `on*` handlers and external resource attributes                                          |
| `Element.prototype.querySelectorAll`                                   | Used by MO observer to scan descendant elements of injected nodes                                                                           |
| `Node.prototype.appendChild`                                           | Used to inject the alert overlay and security veil into the DOM via captured ref                                                            |
| `Node.prototype.removeChild`                                           | Used by MO scanner to remove injected external `<script>` elements from the DOM                                                             |
| `Array.prototype[Symbol.iterator]`                                     | Poisoning this would silently skip validation loops                                                                                         |

**Dead man's switch heartbeat** — every tick dispatches a heartbeat event with a **monotonic counter** that increments inside the private closure. The application only accepts events where the counter is strictly greater than the last seen value **and** within a bounded window (guards against injection of extremely large counter values that would permanently desync the heartbeat). An attacker cannot read or predict the counter from outside the closure. If more than 3 seconds pass without a valid heartbeat, the watchdog has been killed and all open containers are **automatically locked** — derived keys are wiped from memory.

**App function integrity** — at window `load`, all critical application functions are wrapped through `_mkProxy` — the same opaque-forwarder pattern used by network and DOM hooks. The original implementations become closure-private; `toString()` on the live property reveals only the thin forwarder body. Proxied references are frozen into a snapshot and compared by identity on every watchdog tick. A Self-XSS attack that replaces any of these is detected on the very next tick and triggers a full threat response. Raw (unwrapped) references to VFS.init and WinManager.closeAll are kept separately for direct invocation inside `_wipeAppState`. Covered functions: `Crypto.encrypt/decrypt/encryptBin/decryptBin/deriveKey/deriveKeyAndRaw` (exfiltration-path), `Crypto.importRawKey` (called externally — replacement would capture raw AES key bytes), `Crypto.checkVerification` (called on every unlock — replacement with `() => true` bypasses authentication), `Crypto.makeVerification`, `App.lockContainer`, `VFS.init`, `WinManager.closeAll`.

**Scope shadowing guard** — the app's encryption module and the browser's built-in `window.Crypto` (WebCrypto API) share the same identifier. The watchdog confirms it is checking the correct object (app module vs. WebCrypto) by probing for app-specific methods, eliminating false positives.
<a id="proactive-watchdog-resilience"></a>

### Watchdog resilience

The watchdog cannot be killed by a single call to `clearInterval` or by replacing a single timer API. **Four independent mechanisms** run in parallel:

| Mechanism                     | Interval | Kill vector                                                                                                                                       |
| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setInterval`                 | 50 ms    | `clearInterval` with the correct ID                                                                                                               |
| Recursive `setTimeout`        | 937 ms   | `clearTimeout` with the correct ID                                                                                                                |
| `requestAnimationFrame` chain | ~980 ms  | `cancelAnimationFrame` — **guarded: the rAF chain ID is tracked and any call to `cancelAnimationFrame` with that exact ID is silently swallowed** |
| `MessageChannel` self-ping    | 800 ms   | Replacing `setInterval`/`setTimeout`/`cancelAnimationFrame` has zero effect — `MessageChannel` is a separate browser message-queue mechanism      |

All timer functions are captured at startup so even if `window.setInterval` is later replaced, the watchdog timers were already started with the native versions.

**Timer ID protection** — `window.clearInterval`, `window.clearTimeout`, and `window.cancelAnimationFrame` are replaced with guarded versions that silently ignore any attempt to cancel or clear watchdog timer, rAF, or active debugger-trap interval IDs. Legitimate application code that calls these functions on its own IDs is unaffected.

Watchdog timer IDs are stored using plain object properties and tested with the `in` operator — not `Set`. This is intentional: `Set.prototype.has` could be replaced via Self-XSS to let an attacker's timer IDs pass through the guard undetected. The `in` operator is a language-level construct — it cannot be overridden from userland JS.

**Visibility-change fast check** — when the tab transitions from background to visible, an immediate full tick runs so an attacker cannot exploit the ~50 ms inter-tick window while the tab was hidden.

<a id="proactive-excluded-checks"></a>

### Intentionally excluded from checks

| API                                          | Reason                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `console` namespace                          | Overrides are common and benign (DevTools, logging libraries)                                                                                                                                                                                                                                                                                                                                              |
| `Function.prototype.toString`                | Bootstrap-validated at init time by structural checks (name, arity, string coercion). Live periodic checks cause false positives because extensions (Adblock, Dark Reader) routinely wrap `toString`                                                                                                                                                                                                       |
| `document.createElement` _(non-iframe tags)_ | Extensions legitimately create elements (including `<script>`) for their content scripts; blocking all tags causes widespread false positives. **`<iframe>` is the only exception — it is blocked post-init (D3, see below).** `<script>` elements with an external `src=` injected dynamically after page load are silently removed from the DOM and logged to the console — no full modal alert is shown |
| `JSON.stringify` / `JSON.parse`              | DevTools, debugger extensions, and frameworks actively patch these                                                                                                                                                                                                                                                                                                                                         |
| `Promise` / `Promise.prototype.then`         | Polyfills and extensions wrap these regularly                                                                                                                                                                                                                                                                                                                                                              |
| `performance.now`                            | Privacy extensions (Brave, uBlock) intentionally add timing jitter                                                                                                                                                                                                                                                                                                                                         |
| `Object.defineProperty`                      | Too many legitimate uses across extensions and frameworks                                                                                                                                                                                                                                                                                                                                                  |

<a id="proactive-network-interception"></a>

### Network request interception

Every outbound request is validated against `window.location.origin` before it is allowed to proceed. The origin check uses a **fail-closed** design: URLs that cannot be parsed by the `URL` constructor are treated as external (unsafe by default). `data:` URLs (inline resources such as canvas-generated thumbnails) are whitelisted using pure bracket-indexing comparison (`s[0] === 'd'`) and `_pureSlice` (immune to any prototype poisoning), and browser extension schemes (`chrome-extension:`, `moz-extension:`, `safari-web-extension:`) are allowed to prevent false positives from legitimate user-installed extensions:

-   **`fetch`** — blocked and rejected with an error
-   **`XMLHttpRequest.prototype.open`** — blocked and throws synchronously
-   **`navigator.sendBeacon`** — blocked and returns `false`
-   **`WebSocket`** — connections to any **host:port** combination other than the current origin are blocked and trigger a threat alert. The origin check uses a captured URL constructor (immune to runtime replacement). Same-origin WebSocket connections are forwarded transparently
-   **`window.open`** — external URLs blocked; same-origin popups forwarded normally
-   **`EventSource`** — external URLs blocked at construction; an `EventSource` to an external host would establish a persistent covert SSE channel for data exfiltration
-   **`Worker` / `SharedWorker`** — `data:` URL workers, `blob:` URL workers, and external-URL workers are blocked. SafeNova does not create Workers; a same-origin `blob:` Worker runs in a separate global scope with an unhooked native `fetch`, so even same-origin blob URLs are an exfiltration vector and are rejected unconditionally
-   **`ServiceWorker` registration** — blocked preventively. A rogue Service Worker could intercept all fetches on the next page load and inject code before the guard runs. Existing registrations are removed during the threat response
-   **`setTimeout` / `setInterval` string callbacks** — string-form callbacks (e.g. `setTimeout("eval(code)", 0)`) are stripped to a no-op. Function-form callbacks are forwarded normally
-   **`eval`** — replaced with a non-configurable, non-writable property that throws synchronously; cannot be restored after the guard runs
-   **`new Function()`** — the `Function` constructor is proxied; calls via `new Function(...)` throw synchronously. Plain `Function()` calls without `new` (used by feature detection, extensions, etc.) are forwarded to the native constructor — only the constructor path is dangerous

SafeNova makes no legitimate external network requests; any attempt is by definition suspicious.

<a id="proactive-dom-exfiltration"></a>

### DOM exfiltration defense

A second protection layer covers DOM-level exfiltration vectors — APIs that can inject external resources or redirect the page without going through the network layer directly.

| API                                      | Behaviour                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Element.setAttribute`                   | External URLs in resource attributes (`src`, `href`, `data`, `ping`, `action`, `formaction`, `srcset`, `poster`) are blocked and trigger a full alert. Inline `on*` event handler attributes are stripped. Navigation elements (`<a>`, `<area>`) are exempt from the `href` check — those are user-activated navigation, not resource loaders; `ping=` remains blocked                                         |
| `Element.innerHTML` / `outerHTML` setter | HTML strings are scanned for external resource URLs and `on*` attribute patterns before assignment; blocked if a threat is found                                                                                                                                                                                                                                                                               |
| `Element.insertAdjacentHTML`             | Same scan as `innerHTML`                                                                                                                                                                                                                                                                                                                                                                                       |
| `document.write` / `document.writeln`    | Same scan; blocked before the string is written to the document                                                                                                                                                                                                                                                                                                                                                |
| `Location.assign` / `Location.replace`   | External URLs blocked; same-origin navigation forwarded normally                                                                                                                                                                                                                                                                                                                                               |
| `Location.href` setter                   | Same as `assign` / `replace`                                                                                                                                                                                                                                                                                                                                                                                   |
| `HTMLFormElement.submit`                 | External `action=` URLs blocked                                                                                                                                                                                                                                                                                                                                                                                |
| Resource property setters                | `HTMLImageElement.src`, `HTMLScriptElement.src`, `HTMLIFrameElement.src`, `HTMLVideoElement.src`, `HTMLAudioElement.src`, `HTMLEmbedElement.src`, `HTMLObjectElement.data`, `HTMLLinkElement.href` — external URL assignments are blocked. For `<script>.src` specifically the block is **silent**: the element is not loaded, a console trace is written, but no modal alert is shown (reduces alert fatigue) |

**Post-init iframe block (D3)** — after the startup iframe-restoration phase completes and the temporary iframe is removed from the DOM, `document.createElement('iframe')` is permanently intercepted. Any subsequent attempt to create an `<iframe>` triggers a full threat alert. This closes the _fresh-realm native-reset_ attack vector: an attacker who gains JS execution after daemon.js has run could otherwise create their own `about:blank` iframe, pull native function references from its unhooked context (which pass `_isNative()` checks because they genuinely are native), and silently overwrite all daemon hooks. All other tags pass through unmodified — extensions creating `<script>` or any other element are unaffected. The hook is self-healing: the watchdog re-installs it every tick if it is removed.

**MutationObserver defense-in-depth** — a `MutationObserver` watches the entire document subtree and catches attacks that bypass the property/method hooks above:

-   **Added elements** — newly appended nodes are scanned for external resource attributes or `on*` handlers; threatening attributes are removed and a full alert fires
-   **Dynamically injected `<script src="https://...">` elements** — silently removed from the DOM; a console trace is written via a tamper-proof console reference (immune to `console.error` replacement after load). Same-origin, relative-path, and inline `<script>` elements are left untouched
-   **Attribute mutations** — attribute changes on existing elements that add an external resource URL or an `on*` handler are caught and reversed; `href` changes on `<a>` and `<area>` elements are excluded (navigation-only)

---

<a id="proactive-threat-response"></a>

### Threat response

When a native function purity check, App function integrity check, or network request interception fires:

1. **Immediately wipe** all session keys from both `localStorage` and `sessionStorage` using captured native storage references (bypasses any hook placed on the Storage API by the attacker)
2. **Clear Service Workers and Cache API** — unregisters active Service Workers and wipes all browser cache data. This prevents an attacker from spawning a rogue Service Worker for persistence or stashing intercepted data asynchronously
3. **Directly zero in-memory app state** — all sensitive state (encryption key, container data, clipboard, thumbnail cache) is nullified directly, bypassing the event system. Critical cleanup functions are invoked using **captured references** snapshotted at window `load`, so a console-level replacement before the threat fires cannot prevent cleanup
4. Dispatch a lock event — the application locks all open containers via the normal code path, clearing derived keys from memory
5. **Console threat log** — a styled `console.error` with a red background is emitted via a tamper-proof console reference, providing a forensic trace that cannot be suppressed
6. **Debugger trap** — a `debugger` statement fires every 50 ms for up to 5 minutes. If the attacker has DevTools open, the JS engine pauses at each breakpoint, blocking further console commands. When DevTools are closed, this is a native no-op with zero performance cost
7. Show a **security alert overlay** identifying the blocked operation and advising the user to audit browser extensions, with a reload button. The overlay uses a **closed Shadow DOM** so its contents cannot be queried or mutated from `document` scope; a self-healing `MutationObserver` re-appends the overlay if an attacker removes it from the DOM (active for 3 minutes)

Alerts are rate-limited to one per 10 seconds to prevent alert spam while still reporting every distinct threat.

---

<a id="proactive-design-philosophy"></a>

### Design philosophy

SafeNova Proactive is built around one central principle: **protect as many JS primitives and APIs as possible, while using them as little as possible itself.**

All security-critical references are captured once at the very top of execution into a frozen snapshot — a frozen image of the JS runtime taken before any extension or injected script can interfere. From that point on, the guard never calls live globals. Every internal operation that needs a JS built-in goes through the captured snapshot:

| Instead of...                                   | Proactive uses...                                            | Why                                                                                                                                                                                               |
| ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `window.fetch`                                  | Captured reference                                           | Immune to post-load `window.fetch = ...` replacement                                                                                                                                              |
| `Array.prototype.push`                          | `arr[arr.length] = x`                                        | Index assignment cannot be hooked                                                                                                                                                                 |
| `for...of`                                      | Indexed `for` loops                                          | Immune to `Symbol.iterator` poisoning                                                                                                                                                             |
| `new Set()` / `Set.prototype.has`               | `key in plainObject`                                         | `in` is a language operator, unhookable                                                                                                                                                           |
| `String.prototype.match` / `.exec`              | Manual `indexOf` loops                                       | Avoids hookable regex prototype methods                                                                                                                                                           |
| `String.prototype.startsWith`                   | `_pureSlice(s, 0, n) === prefix`                             | Pure bracket + concatenation; zero prototype calls                                                                                                                                                |
| `String.prototype.toLowerCase` / `.toUpperCase` | `_pureToLower(s)`                                            | Frozen A-Z→a-z lookup + bracket indexing; no prototype dependency at all. Even if every `String.prototype` method is replaced, `_pureToLower` is unaffected                                       |
| `String.prototype.indexOf` / `.substring`       | `_pureIndexOf(s, needle, from)`                              | Nested indexed loop with bracket comparison; no prototype dependency. Immune to `indexOf = () => -1` or `substring = () => ''` attacks                                                            |
| `String.prototype.slice`                        | `_pureSlice(s, start, end)`                                  | Bracket indexing + `+=` concatenation; no prototype dependency. Used for URL extraction, key prefix checks, and all substring operations                                                          |
| `String.prototype.charCodeAt`                   | `s[0] === 'd'` (bracket indexing + `===`)                    | Pure operator; the only `charCodeAt` call site (data: URL check) was replaced with a direct single-char bracket comparison                                                                        |
| `Array.prototype.forEach`                       | Indexed `for` loops                                          | Unhookable loop construct                                                                                                                                                                         |
| `String(x)`                                     | `'' + x`                                                     | Concatenation operator, not a function call                                                                                                                                                       |
| `Array.prototype.slice`                         | Captured reference                                           | Survives prototype replacement                                                                                                                                                                    |
| `fn.apply(ctx, args)` / `fn.call(ctx, args)`    | `_reflectApply(fn, ctx, args)`                               | `.apply()` / `.call()` resolve through live `Function.prototype`; if replaced post-boot, all pass-through calls are hijacked. `Reflect.apply` is captured at boot and goes directly to `[[Call]]` |
| `instanceof Request`                            | `typeof input === 'object' && typeof input.url === 'string'` | `Symbol.hasInstance` can be replaced to make `instanceof` return false, bypassing URL extraction in the fetch hook                                                                                |
| `el.appendChild(child)` / `el.removeChild(c)`   | `_reflectApply(_nodeAppend, parent, [child])`                | Live `Node.prototype.appendChild/removeChild` could be replaced to silently prevent alert overlay injection or prevent removal of malicious `<script>` elements                                   |
| `el.querySelectorAll('*')`                      | `_reflectApply(_N.elQuerySelectorAll, el, ['*'])`            | Live method could return empty NodeList, letting child elements of injected nodes bypass the MO scanner                                                                                           |
| `el.removeAttribute(name)`                      | `_reflectApply(_N.removeAttribute, el, [name])`              | Live method could be hooked to prevent stripping of malicious `on*` handlers or external resource attributes                                                                                      |
| `Object.prototype.hasOwnProperty.call()`        | `'key' in obj`                                               | `in` is a language operator; `hasOwnProperty.call` goes through live `Function.prototype.call`                                                                                                    |

As a direct result, the integrity-checking core is **well-isolated and resistant to most hook-based attacks**: replacing `window.fetch`, `Array.prototype.push`, `String.prototype.toLowerCase`, `Function.prototype.call`, `Function.prototype.apply`, or any other live global after page load cannot change the guard's behaviour — it uses pure operators for all string processing, `Reflect.apply` for all internal function invocations, captured native DOM methods for overlay/scanner operations, validates captured references on every watchdog tick, and would detect the replacement before an attacker could leverage it.

<a id="proactive-hook-opacity"></a>

#### Hook opacity

Every public hook function placed on `window` or prototypes — as well as all guarded application functions — is wrapped in an opaque forwarder. When an attacker inspects hooked functions via `toString()` in the DevTools console, they see only the forwarder shell — the actual security logic is hidden inside the closure and unreachable from outside. **All hooks and app functions** share this identical opaque signature:

| Category                | Hooks                                                                                                                                                                                          |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Network**             | `fetch`, `XMLHttpRequest.prototype.open`, `navigator.sendBeacon`, `WebSocket`, `window.open`, `EventSource`                                                                                    |
| **DOM exfiltration**    | `Element.prototype.setAttribute`, `innerHTML` setter, `outerHTML` setter, `insertAdjacentHTML`, `document.write`, `document.writeln`, `Location.assign/replace/href`, `HTMLFormElement.submit` |
| **Resource properties** | `img.src`, `script.src`, `iframe.src`, `video.src`, `audio.src`, `embed.src`, `object.data`, `link.href`                                                                                       |
| **Code injection**      | `window.eval`, `window.Function` (`new Function()` is blocked; plain `Function()` calls are forwarded — only the constructor path is dangerous)                                                |
| **Timer guards**        | `setTimeout` (string callback), `setInterval` (string callback)                                                                                                                                |
| **Watchdog protection** | `clearInterval`, `clearTimeout`, `cancelAnimationFrame`                                                                                                                                        |
| **Workers**             | `Worker`, `SharedWorker`, `ServiceWorkerContainer.register`                                                                                                                                    |
| **App functions**       | `Crypto.encrypt/decrypt/encryptBin/decryptBin/deriveKey/deriveKeyAndRaw/importRawKey/checkVerification/makeVerification`, `App.lockContainer`, `VFS.init`, `WinManager.closeAll`               |

---

<a id="container-integrity-scanner"></a>

## 🛡️ Container Integrity Scanner

> ![](./pics/screenshot_integrity_scanner.png)

The built-in scanner performs a deep analysis of the virtual disk image, encrypted file table, folder hierarchy, desktop layout, and workspace environment. It runs **28 checks** in two phases:

<a id="scanner-phase-1"></a>

### Phase 1 — VFS structural checks (21 steps, synchronous)

| #   | Check                        | Repairs                                                                        |
| --- | ---------------------------- | ------------------------------------------------------------------------------ |
| 1   | Root node integrity          | Recreates missing root; fixes type and parentId                                |
| 2   | Node field validation        | Fixes IDs, names, types; restores missing/invalid ctime and mtime to today     |
| 3   | Node ID format validation    | Reassigns malformed IDs; migrates position data                                |
| 4   | Timestamp anomaly detection  | Detects mass-identical ctimes; spreads them across a 1-second window on repair |
| 5   | File name validation         | Sanitizes invalid characters, truncates long names                             |
| 6   | Orphaned node detection      | Reattaches to root                                                             |
| 7   | Parent type validation       | Reattaches nodes whose parent is a file                                        |
| 8   | Parent-child cycle detection | Breaks cycles by reattaching to root                                           |
| 9   | Node reachability analysis   | O(n) memoized; reattaches unreachable nodes                                    |
| 10  | Timestamp integrity          | Fixes invalid/future timestamps                                                |
| 11  | File size validation         | Resets negative/invalid sizes                                                  |
| 12  | File metadata validation     | Strips unknown properties                                                      |
| 13  | Duplicate name detection     | Auto-renames collisions                                                        |
| 14  | Empty folder chain detection | O(n) iterative post-order DFS; informational                                   |
| 15  | Position table cleanup       | Removes stale entries                                                          |
| 16  | Folder position maps         | Creates missing position maps                                                  |
| 17  | Position entry completeness  | Only checks visited (opened) folders; auto-positions on repair                 |
| 18  | Position collision detection | Relocates overlapping icons                                                    |
| 19  | Grid alignment verification  | Snaps off-grid positions                                                       |
| 20  | Folder depth analysis        | O(n) memoized; warns when nesting > 50 levels                                  |
| 21  | Node count summary           | Informational — file/folder/position counts                                    |

<a id="scanner-phase-2"></a>

### Phase 2 — Database-level checks (7 steps, async)

| #   | Check                        | Repairs                                                                                                             |
| --- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| 1   | File data existence          | Removes VFS nodes whose encrypted blob is missing from IDB                                                          |
| 2   | Encryption IV integrity      | Accepts Array/Uint8Array/ArrayBuffer (canonical: plain Array); coerces base64 strings; purges only if truly invalid |
| 3   | File blob integrity          | Resets declared size to 0 if blob is empty                                                                          |
| 4   | Orphaned storage records     | Deletes DB records not referenced by any VFS node                                                                   |
| 5   | Record container binding     | Fixes records bound to wrong container ID                                                                           |
| 6   | Container size consistency   | Recalculates totalSize from live VFS nodes                                                                          |
| 7   | File decryption verification | Attempts to decrypt each blob; removes files whose ciphertext is unreadable (e.g. corrupted by duress trigger)      |

Before auto-repair runs, a **confirmation dialog** recommends exporting the container as a `.safenova` backup — you can do this without leaving the scanner. After a successful repair, a verification scan runs automatically to confirm all issues are resolved.

If auto-repair cannot fix the remaining issues, a **Deep Clean** option becomes available. It performs an aggressive structural rebuild in five O(n) passes:

1. Scan DB storage records
2. Purge dead nodes — remove every VFS node with no real encrypted data behind it
3. Flatten deep folder chains — files nested more than 50 levels deep are reparented to their closest ≤50-level ancestor; all file data is preserved
4. Repair metadata — each node with a missing or invalid `ctime`/`mtime` gets today's date
5. Clean storage records — remove orphaned DB entries in a single batch transaction

After Deep Clean, a verification scan runs automatically. A backup is offered before Deep Clean runs, same as for auto-repair.

---

<a id="performance"></a>

## ⚡ Performance

SafeNova schedules AES-GCM operations to run with maximum concurrency, taking full advantage of hardware AES acceleration exposed by the browser’s Web Crypto API.

<a id="adaptive-concurrency"></a>

### Adaptive concurrency

The degree of parallelism is computed once at startup based on `navigator.hardwareConcurrency`, capped at 8. This serves as the default batch width for all bulk encrypt/decrypt loops. On an 8-core machine, up to 8 files are processed simultaneously.

<a id="bulk-upload"></a>

### Bulk upload

For each batch of files the application reads all `ArrayBuffer` payloads in parallel, encrypts the batch in parallel, then writes every encrypted record to IDB in a **single transaction**, eliminating the per-file transaction overhead that would otherwise dominate for large numbers of small files. Files with encrypted blobs exceeding **50 MB** are stored as split 50 MB chunks across the `chunks` object store, avoiding the browser's ~2 GB structured-clone limit on IDB reads; the chunking is fully transparent to all read paths.

<a id="zip-export"></a>

### ZIP export

Exporting files as an archive uses a single IDB read transaction that fetches all required records concurrently. Decryption of all records is then dispatched in one parallel batch rather than being serialised sequentially.

<a id="password-change"></a>

### Password change

Re-encrypting a container under a new key dispatches all decrypt–re-encrypt pairs for every file **fully in parallel**. Results are accumulated and written back in a single batch, reducing total elapsed time to approximately one parallel round-trip plus one database write.

<a id="container-export"></a>

### Container export

Exporting a `.safenova` file requires no single contiguous memory allocation regardless of container size. The builder receives each file blob as an individual chunk (no concatenation into one giant buffer), computes CRC32 incrementally, and emits an **array of small output parts**. The parts array is passed directly to the `Blob` constructor — the browser stitches the pieces together internally without requiring a duplicate allocation. The peak RAM footprint for an N-gigabyte export is approximately N bytes (the data already held in IDB), rather than ~3× N.

<a id="drag-drop-performance"></a>

### Drag-and-drop performance (large folders)

Icon dragging in folders with many files previously rebuilt the occupied-cell map on **every** mouse/touch frame (~60 fps). With hundreds of files this became a measurable bottleneck. The hot path is now O(1) per frame:

-   **Touch drag** — the occupied map is built once at drag-start (when the 400 ms long-press fires) and reused throughout the gesture
-   **Mouse drag** — occupied maps are computed once at drag-start and once when the pointer first enters a drop target, not on every frame
-   **Snap preview throttle** — snap-preview positions are recomputed only when the pointer crosses a grid cell boundary (96 px steps), not on every pixel movement
-   **No full map clone** — snap previews use a small overlay map (one entry per selected item) instead of cloning the full occupied map on each call

---

<a id="mobile-touch-support"></a>

## 📱 Mobile Touch Support

SafeNova is fully usable on touchscreen devices (Android Chrome, iOS Safari). All gesture interactions work on real hardware, not only in DevTools device emulation.

<a id="mobile-long-press"></a>

### Long-press to drag

Holding a finger on an icon for **400 ms** activates drag mode (haptic feedback where the OS supports it). The `touchstart` handler is registered as `{ passive: false }` on the icon area and immediately calls `e.preventDefault()` when the touch lands on an icon. This suppresses the native Android long-press gesture (which would otherwise fire `touchcancel` + `contextmenu` at ~500 ms and silently kill the drag). Scrolling on empty area is unaffected — `preventDefault` is only called when a `.file-item` is the touch target, and `.file-item` elements carry `touch-action: none` in CSS to prevent the browser's pan gesture recognizer from competing.

<a id="mobile-multi-file-drag"></a>

### Multi-file drag

All items in the current selection are dragged simultaneously. Each selected icon follows the same displacement vector as the primary icon. Snap previews are shown for every item in the selection, offset relative to one another to reflect final grid positions.

<a id="mobile-context-menu"></a>

### Context menu

A short tap (< 350 ms) on an icon opens the context menu. A long press (≥ 400 ms) starts a drag instead of opening the menu. The two actions are mutually exclusive — if the native `contextmenu` event fires while a drag is already active, it is suppressed; if it fires before the drag timer completes, the timer is cancelled.

<a id="mobile-paste-at-finger-position"></a>

### Paste at finger position

When **Paste** is triggered from the context menu on a touch device, the items are placed at the position where the menu was opened, rather than defaulting to the origin. The screen position is captured when the menu action is confirmed, and each pasted item is snapped to the nearest free grid cell relative to that position.

<a id="mobile-overscroll"></a>

### Overscroll

`overscroll-behavior: none` is applied to `.desktop-area` and `.fw-area` to prevent pull-to-refresh and iOS overscroll bounce from interfering with drag gestures.

---

<a id="security-audit"></a>

## 🛡️ Security Audit Changelog

A detailed record of the most impactful security fixes and hardening steps applied during internal audits:

-   **Security Audit Changelog**: You find it [here](./SECURITY_AUDIT.md)

---

<a id="contribute"></a>

## 🛠️ Contributing

If you want to contribute check our Contributions guideline first:

-   **Contribution guideline**: You find it [here](https://github.com/DosX-dev/SafeNova/blob/main/CONTRIBUTING.md)

---

<a id="community"></a>

## 💬 Community

Have questions, ideas, or just want to chat? Here's where to find us:

-   **GitHub Issues**: Report bugs or request features via [Issues](https://github.com/DosX-dev/SafeNova/issues)

---

<a id="thanks"></a>

## 🤝 Thanks to all contributors

I (**[DosX](https://github.com/DosX-dev)**) envision **SafeNova** as what it is: a complex engineering product — something created to solve a real problem, not just to sit on a shelf. Nevertheless, I would like to point out that the level of quality and safety achieved in this project is not a purely individual achievement. The architecture, the threat model, the complex cases that I would never have thought to talk about on my own — all this was created with the help of people who really helped, asked difficult questions and identified what I had missed. Great credit goes to everyone who left a review, suggestion, or error message along the way. If you want to contribute, check out the [contribution section](https://github.com/DosX-dev/SafeNova?tab=contributing-ov-file).

<a href="https://github.com/DosX-dev/SafeNova/graphs/contributors">
<img src="https://readme-contribs.as93.net/contributors/DosX-dev/SafeNova?textColor=737373&perRow=9&shape=squircle&isResponsive=true" />
</a>

> Special thanks:
>
> -   **[Joe12387](https://github.com/Joe12387)** — Private/Incognito detection implementation in TypeScript that was rewritten in JavaScript

> The following tools were used during development:
>
> -   **[Visual Studio Code](https://code.visualstudio.com/)** — writing code in it and formatting
> -   **[Web DevTools](https://en.wikipedia.org/wiki/Web_development_tools)** — debugging and inspecting web applications
> -   **[Detect It Easy](https://github.com/horsicq/Detect-It-Easy)** — analyzing the ZIP format and structure in practice
> -   **[Claude Opus Agent (v4.6)](https://www.claude.ai/)** — help with developing the interface part, checking the code and writing local tests


================================================
FILE: SECURITY_AUDIT.md
================================================
# 🛡️ Security Audit Changelog

> This document summarizes the most significant security-related changes introduced during internal audits of the SafeNova codebase. Each entry represents a hardening step that meaningfully raises the security posture of the project.
>
> Entries are ordered newest-first. The **Area** column indicates which subsystem was affected:
>
> -   **SafeNova Proactive** — the runtime anti-tamper guard (`src/js/proactive/daemon.js`)
> -   **SafeNova Core** — the application itself (crypto, state, VFS, UI, DB)

---

## Audit Results

| Commit    | Area               | What was fixed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Security impact                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `b2fa4fe` | SafeNova Core      | Fixed three bugs discovered during internal audit: (1) `_corruptFileBlobs` in `db.js` — duress IV-zeroing was a no-op for chunked files because `rec.iv` is stored as a plain JS `Array` (via `Array.from()`), not an `ArrayBuffer` or `TypedArray`; the old code called `.iv.buffer` on a plain array and got `undefined`, so `new Uint8Array(undefined)` produced a zero-length view and `.fill(0)` did nothing — the IV was never actually zeroed; fixed by branching on `Array.isArray`, `instanceof ArrayBuffer`, and `ArrayBuffer.isView`. (2) `_reassemble` in `db.js` — if any chunk was absent from IDB the missing slot was silently skipped: `totalSize` was not reduced and all subsequent chunks were written at a shifted offset, producing garbled plaintext with no error; fixed by rejecting the promise the moment all reads complete if any `parts[k]` slot is empty. (3) `downloadFile` in `fileops.js` — lacked the empty-file guard that `openFile` already had; calling `Crypto.decryptBin` on a `null`/zero-length blob threw an uncaught exception and crashed the download; fixed by mirroring the `openFile` check and returning an empty `ArrayBuffer` for empty files. | (1) Duress protection was completely bypassed for large (chunked) files — the IV was left intact, making those files decryptable after a panic trigger. (2) A single missing IndexedDB chunk caused silent data corruption instead of a detectable error. (3) A zero-byte file caused an unhandled crash on export. |
| `606ffdc` | SafeNova Proactive | Captured `MessagePort.prototype.postMessage` into `_N.portPostMessage`; added it to `_CAPTURE_MUST_BE_NATIVE` and `_NATIVE_CHECKS`; routed both MC watchdog self-ping call sites through `_reflectApply(_N.portPostMessage, _mc.port1, [null])`; introduced `_RESOURCE_ATTRS_NO_SRCSET` (identical to `_RESOURCE_ATTRS` minus `srcset`) and switched `_scanElementForThreats` and the MO attribute-change path to use it — `srcset` is kept in `_RESOURCE_ATTRS` so the `setAttribute`/innerHTML hooks (which receive individual tokens) still block srcset-based exfiltration | Closes the MC watchdog kill-switch: a post-boot replacement of `MessagePort.prototype.postMessage` could silently stop the fourth watchdog mechanism (the other three remain active, but defence-in-depth is weakened); fixes a high-severity false positive where any element with a responsive-image `srcset` (e.g. `"photo.jpg 2x"`) caused `_isExternal` → `new URL()` to throw, the fail-closed return triggered an immediate alert and emergency key wipe — destroying the user's data on a completely legitimate page |
| `ede90fe` | SafeNova Proactive | Captured `Promise.prototype.then` into `_N.promiseThen` and `ServiceWorkerRegistration.prototype.unregister` into `_N.swUnregister`; added both to `_CAPTURE_MUST_BE_NATIVE` and `_NATIVE_CHECKS`; rewrote `_nukeCachesAndWorkers` to use captured refs so `Promise.prototype.then` or `.unregister` replacement cannot silently kill the cache/SW wipe; changed `_showAlert` healer `MutationObserver` target from `document.body` to `document.documentElement` (subtree: true) so replacing `document.body` after render does not leave the healer dead on a detached node; fixed `_pureCollapseAttrSpaces` to track quote state so spaces around `=` inside quoted attribute values (e.g. URL query strings) are no longer collapsed | Closes the `Promise.prototype.then` replacement bypass that turns the entire cache and Service Worker wipe into a silent no-op; prevents an attacker from keeping a rogue Service Worker alive after a threat response by replacing `.unregister`; ensures the security overlay self-healer survives `document.body` replacement (attacker gains persistent removal of the warning); prevents URL corruption in alert messages caused by collapsing `=` spaces inside quoted values |
| `93d5de5` | SafeNova Proactive | Added `_pureCollapseAttrSpaces` to normalise whitespace around `=` before attribute scanning in `_htmlHasThreat`; captured `Date.now`, `Location.prototype.reload`, `CacheStorage.prototype.keys/delete`, `ServiceWorkerContainer.prototype.getRegistrations` into `_N` with validation in `_CAPTURE_MUST_BE_NATIVE` and `_NATIVE_CHECKS`; rewrote `_nukeCachesAndWorkers` to use captured object refs and `_reflectApply`; replaced all `Date.now()` and `window.location.reload()` call sites with captured equivalents; replaced `window.addEventListener` fallback in `_showAlert` with captured `_N.addEventListener`; replaced `instanceof Set` with a structural duck-type check | Closes the HTML attribute whitespace-bypass (`src = "url"` was invisible to the scanner); makes alert rate-limiting, healer deadline, debugger-trap timeout, and post-alert reload tamper-proof; prevents live `window.caches`/`navigator.serviceWorker` replacement from silently neutering the threat-response cache wipe; guards the `DOMContentLoaded` alert fallback against `addEventListener` replacement; prevents `Symbol.hasInstance` poisoning from skipping selection clear during emergency state wipe |
| `0a7add8` | SafeNova Proactive | Fixed dead man's switch false-triggering on fresh browser sessions: the 50 ms `setInterval` in daemon.js could advance `_heartbeatN` far beyond the +15 upper bound before `main.js` registered its `snv:alive` listener; added a one-time bootstrap exemption that skips the upper-bound check on the very first accepted heartbeat                                                                                                                                                                                                                                                                                                                                                    | Eliminates a denial-of-service race condition where a legitimate user on a cold cache was immediately locked out after unlock, while preserving the protection against an attacker dispatching a fake `snv:alive` with `n = Number.MAX_SAFE_INTEGER` to silence real heartbeats                                                                                                                                                                                                                                     |
| `78af635` | SafeNova Proactive | Wrapped critical `Crypto.*` and `App.*` methods with closure-private proxies; `toString()` now shows only a thin forwarder; frozen references used for per-tick tamper detection                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Prevents an attacker from reading or replacing encryption/decryption routines at runtime; makes devtools inspection of real implementations infeasible                                                                                                                                                                                                                                                                                                                                                              |
| `47f7a83` | SafeNova Proactive | Replaced live `.call`/`.apply` usage with a captured `Reflect.apply`; captured additional DOM methods (`appendChild`, `removeChild`, `querySelectorAll`, `removeAttribute`) and routed all overlay/observer operations through them                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Eliminates a class of attacks that poison `Function.prototype.call`/`.apply` to intercept every hooked call; DOM operations can no longer be subverted by redefining prototype methods                                                                                                                                                                                                                                                                                                                              |
| `967bc1f` | SafeNova Proactive | Introduced pure operator-level string utilities (`_pureToLower`, `_pureIndexOf`, `_pureSlice`) that use only bracket indexing and comparisons — no prototype method calls                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Makes all string processing inside the daemon immune to `String.prototype` poisoning, closing a subtle bypass vector for URL/attribute checks                                                                                                                                                                                                                                                                                                                                                                       |
| `9f48c33` | SafeNova Proactive | Reduced watchdog `setInterval` tick from 1 000 ms to 50 ms                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Shrinks the window in which a tamper can exist undetected from ~1 s to ~50 ms, drastically limiting the usefulness of race-condition attacks                                                                                                                                                                                                                                                                                                                                                                        |
| `8c8579c` | SafeNova Proactive | Captured `String.prototype.toLowerCase`/`.indexOf`; added fail-closed handling for unparseable URLs; blocked `blob:` Worker/SharedWorker URLs; added a fourth watchdog mechanism (MessageChannel self-ping)                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Closes live-prototype bypass paths in DOM hooks and URL checks; prevents covert same-origin workers from running attacker code outside the daemon's control                                                                                                                                                                                                                                                                                                                                                         |
| `0ee205b` | SafeNova Proactive | Blocked `<iframe>` creation after init (D3 defense); intercepted `document.createElement('iframe')` to return a harmless `<div>`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Prevents the fresh-realm native-reset attack where an attacker spawns an iframe to obtain untampered `Function`, `Object`, etc. and circumvent all Proactive hooks                                                                                                                                                                                                                                                                                                                                                  |
| `f2f64a8` | SafeNova Proactive | Added DOM exfiltration defenses: hooked `setAttribute`, `innerHTML`/`outerHTML`, `insertAdjacentHTML`, `document.write`, `HTMLFormElement.submit`, resource property setters, `window.open`, `EventSource`, `Worker`/`SharedWorker`, `setTimeout`/`setInterval` string callbacks; blocked `eval` and `new Function()`                                                                                                                                                                                                                                                                                                                                                                   | Covers the major DOM-based data-exfiltration and dynamic-code-injection vectors; a malicious extension can no longer silently smuggle plaintext out through HTML attributes, navigation, or injected scripts                                                                                                                                                                                                                                                                                                        |
| `ae415ac` | SafeNova Proactive | Bumped daemon to v6; replaced all `Set`/`forEach`/`repeat`/`replace` usage with index-based loops and plain objects; added MessageChannel self-ping; tightened heartbeat monotonicity bounds                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | Eliminates dependency on `Set`/`Array` iterators and prototype methods inside the daemon itself — a tampered iterator can no longer crash or silently skip the watchdog                                                                                                                                                                                                                                                                                                                                             |
| `daffd47` | SafeNova Proactive | Moved hook logic into closure-private `_*Impl` functions; public forwarders on `_H` are one-liners whose `toString()` reveals nothing                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Raises the bar for reverse-engineering hook internals; devtools `toString()` inspection no longer exposes the URL-matching or blocking logic                                                                                                                                                                                                                                                                                                                                                                        |
| `7cab50f` | SafeNova Proactive | On threat alert: unregister all Service Workers and delete all CacheStorage entries                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Prevents a persistent attacker payload from surviving a page reload via Service Worker cache; ensures the threat response leaves no injectable residue                                                                                                                                                                                                                                                                                                                                                              |
| `5e97757` | SafeNova Proactive | Upgraded to v4; added pre-capture validation (verify natives are genuine before boot); triple-redundant watchdog (setInterval + recursive setTimeout + rAF); dead-man heartbeat with auto-lock; closed-ShadowDOM alert host with session-unique class                                                                                                                                                                                                                                                                                                                                                                                                                                   | Pre-capture validation blocks "early bird" attacks that replace natives before the daemon loads; triple redundancy makes it practically impossible to silently kill the watchdog; cosmetic-filter-resistant alert overlay cannot be hidden by ad blockers or extensions                                                                                                                                                                                                                                             |
| `dc3025d` | SafeNova Proactive | Captured raw `localStorage`/`sessionStorage` references; two-pass storage wipe (overwrite with zeros → delete); dispatched `snv:lock` event on threat                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | Getter-level interception of `window.localStorage` can no longer suppress key wiping; zero-overwrite before deletion prevents data remanence on storage engines that defer physical erasure                                                                                                                                                                                                                                                                                                                         |
| `d88dc63` | SafeNova Proactive | Initial introduction of the SafeNova Proactive runtime guard — native capture at startup, fetch/XHR/sendBeacon hooks, 1 s watchdog, storage wipe, alert overlay                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Established the foundational anti-tamper and anti-exfiltration layer; without this commit, no runtime integrity guarantees existed                                                                                                                                                                                                                                                                                                                                                                                  |
| `78af635` | SafeNova Core      | Wrapped `VFS.init` and `WinManager.closeAll` with proxied references for use in `_wipeAppState`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Ensures the emergency state-wipe routine always calls the real methods even if the live references on the module objects have been overwritten                                                                                                                                                                                                                                                                                                                                                                      |
| `c4adf7f` | SafeNova Core      | AES-GCM-wrapped the per-tab session key (`snv-sk`); bound browser fingerprint to `navigator.userAgent` and `navigator.platform`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | A raw sessionStorage dump no longer yields the plain session key; copying storage blobs to a different browser/OS silently invalidates them                                                                                                                                                                                                                                                                                                                                                                         |
| `8a6d6ed` | SafeNova Core      | Added a cookie secret (`snv-kc`) and an IndexedDB secret (`snv-ki`) to the browser wrap-key derivation (three-source HKDF)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Compromising a single storage mechanism (localStorage, cookie, or IDB) is no longer sufficient to reconstruct the wrap key; all three must be present simultaneously                                                                                                                                                                                                                                                                                                                                                |
| `bf6fa26` | SafeNova Core      | Derived a browser-specific HKDF-SHA-256 key from a stable fingerprint and used it to AES-GCM-wrap the persistent `snv-bsk`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Persistent session keys stored in localStorage are now encrypted at rest; offline exfiltration of the storage file does not reveal the key without the matching browser fingerprint                                                                                                                                                                                                                                                                                                                                 |
| `9f962a0` | SafeNova Core      | Introduced a dual-key session model: per-tab `snv-sk` (sessionStorage) and persistent `snv-bsk` (localStorage), both AES-256-GCM encrypted with scoped expiries                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Splits session trust into two independent scopes — a compromised tab key does not unlock the persistent session and vice versa; expiries limit the blast radius of a leaked blob                                                                                                                                                                                                                                                                                                                                    |
| `b986213` | SafeNova Core      | Implemented cross-tab session guard with tab identity, localStorage heartbeat (5 s), 30 s TTL, and force-kick protocol                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Prevents parallel access to the same container from multiple tabs, eliminating a class of race conditions that could corrupt the VFS or leak decrypted state through a forgotten tab                                                                                                                                                                                                                                                                                                                                |
| `9d90b94` | SafeNova Core      | Added duress (panic) password support — silently and irreversibly corrupts all encrypted blobs; erases duress hash and export cache after trigger                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Provides plausible deniability under coercion; the destruction is indistinguishable from a wrong-password attempt and leaves no forensic trace of the duress feature itself                                                                                                                                                                                                                                                                                                                                         |
| `710fac9` | SafeNova Core      | Randomized XOR pre-shred for secure container deletion — random bytes XOR-flipped at random positions before blob removal                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Prevents AES-GCM ciphertext recovery on storage engines that lazily reclaim pages; the overwritten bytes are cryptographically unpredictable                                                                                                                                                                                                                                                                                                                                                                        |
| `7760914` | SafeNova Core      | Migrated session storage from plaintext to AES-256-GCM encrypted blobs; removed legacy plaintext import path and PBKDF2 fallback                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Eliminated the last remaining plaintext credential storage; even if an attacker reads sessionStorage, they obtain only encrypted blobs that require the in-memory key                                                                                                                                                                                                                                                                                                                                               |
| `7a2c7a5` | SafeNova Core      | Sanitized CSS hex colors and HTML-escaped extension text before SVG embedding; `CSS.escape` for selector queries; stripped HTML special chars from filenames                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | Closed XSS / CSS-injection vectors in dynamically generated SVG icons and DOM queries; prevents attacker-crafted filenames from executing code in the UI                                                                                                                                                                                                                                                                                                                                                            |
| `10477c1` | SafeNova Core      | Introduced a strict Content Security Policy (`<meta>` tag + server headers); extracted inline scripts to external files for CSP compliance                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Blocks inline script injection entirely; even if an attacker injects HTML, the browser refuses to execute attacker-supplied `<script>` blocks                                                                                                                                                                                                                                                                                                                                                                       |
| `52de238` | SafeNova Core      | Derived a per-container HKDF-SHA-256 key for the export cache (info `snv-export-cache-v1`) from the Argon2id salt                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Export cache is now browser-independent but container-bound; a stolen cache blob from one container cannot be used to reconstruct another container's manifest                                                                                                                                                                                                                                                                                                                                                      |
| `a9c2edf` | SafeNova Core      | Nullified heavy blobs (`lazyWorkspace`, `_alogZ`, `_exportCache`) in IDB before record deletion on container removal                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Forces the browser to release persistent storage immediately; prevents ghost data from lingering in IDB until lazy garbage collection runs                                                                                                                                                                                                                                                                                                                                                                          |
| `fa0794f` | SafeNova Core      | Added recursion guards (visited sets, depth caps) in `deleteSelected`, `deepCopy`, `_folderSize`, ZIP export; sanitized filenames; hard-capped import size                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Eliminates infinite-loop and stack-overflow vulnerabilities on corrupt/malicious VFS data; blocks filename-based injection and oversized imports that could exhaust memory                                                                                                                                                                                                                                                                                                                                          |
| `6648cf7` | SafeNova Core      | Detected and broken parent↔child cycles in VFS; added visited-set guard to `remove()` to prevent infinite recursion                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | A crafted or corrupted container with cyclic folder references can no longer hang the browser or crash the tab                                                                                                                                                                                                                                                                                                                                                                                                      |
| `2321820` | SafeNova Core      | Encrypted activity logs with AES-GCM before flushing to storage; preserved legacy/compressed migration path                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Activity logs no longer leak operation history in plaintext; even with IDB access, the log content requires the active session key                                                                                                                                                                                                                                                                                                                                                                                  |
| `1adf58c` | SafeNova Core      | Added incognito/private-mode detection using engine-fingerprint checks (no UA sniffing) with a one-time warning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Warns users that containers created in private mode are ephemeral; prevents accidental data loss due to IDB volatility in incognito without relying on spoofable user-agent strings                                                                                                                                                                                                                                                                                                                                 |

---

> **How to read this table**: each row is a single commit that addressed a concrete vulnerability or hardening gap. Commits are grouped by area and sorted by significance. The table is not exhaustive — only changes with a direct, measurable impact on the security model are included.


================================================
FILE: src/.server.ps1
================================================
# Local Dev Server
# Run: right-click -> "Run with PowerShell"  |  or: .\._server.ps1
# Requirements: Windows only, no external dependencies

# UTF-8 output so Cyrillic paths and special chars display correctly in console
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding           = [System.Text.Encoding]::UTF8

# ── Config ────────────────────────────────────────────────────
$PREFERRED_PORT = 7777
$PORT_RANGE_MIN = 3000
$PORT_RANGE_MAX = 9999
$PORT_MAX_TRIES = 20
$root           = $PSScriptRoot.TrimEnd('\') + '\'

# ── MIME types ────────────────────────────────────────────────
$mime = @{
    ".html"  = "text/html; charset=utf-8"
    ".htm"   = "text/html; charset=utf-8"
    ".css"   = "text/css; charset=utf-8"
    ".js"    = "application/javascript; charset=utf-8"
    ".mjs"   = "application/javascript; charset=utf-8"
    ".json"  = "application/json; charset=utf-8"
    ".svg"   = "image/svg+xml"
    ".png"   = "image/png"
    ".jpg"   = "image/jpeg"
    ".jpeg"  = "image/jpeg"
    ".gif"   = "image/gif"
    ".webp"  = "image/webp"
    ".ico"   = "image/x-icon"
    ".woff"  = "font/woff"
    ".woff2" = "font/woff2"
    ".ttf"   = "font/ttf"
    ".otf"   = "font/otf"
    ".mp4"   = "video/mp4"
    ".webm"  = "video/webm"
    ".txt"   = "text/plain; charset=utf-8"
    ".xml"   = "application/xml; charset=utf-8"
}

# ── Port availability check ───────────────────────────────────
# Uses TcpListener to probe the port BEFORE passing it to HttpListener.
# This avoids the misleading generic "Access Denied" exception from HttpListener.
function Test-PortFree([int]$p) {
    try {
        $tcp = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $p)
        $tcp.Start()
        $tcp.Stop()
        return $true
    } catch {
        return $false
    }
}

# ── Port selection ────────────────────────────────────────────
# 1. Try preferred port first.
# 2. Fall back to random ports in range until one is free.
$port = $null

if (Test-PortFree $PREFERRED_PORT) {
    $port = $PREFERRED_PORT
} else {
    Write-Host ""
    Write-Host "  Port $PREFERRED_PORT is busy, picking a random one..." -ForegroundColor DarkYellow

    $rng  = [System.Random]::new()
    for ($i = 0; $i -lt $PORT_MAX_TRIES; $i++) {
        $candidate = $rng.Next($PORT_RANGE_MIN, $PORT_RANGE_MAX + 1)
        if (Test-PortFree $candidate) {
            $port = $candidate
            break
        }
    }
}

if ($null -eq $port) {
    Write-Host ""
    Write-Host "  [!] Could not find a free port after $PORT_MAX_TRIES attempts." -ForegroundColor Red
    Write-Host "      Range: $PORT_RANGE_MIN-$PORT_RANGE_MAX" -ForegroundColor Red
    Write-Host ""
    Read-Host "  Press Enter to exit"
    exit 1
}

# ── Start listener ────────────────────────────────────────────
$prefix   = "http://localhost:$port/"
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add($prefix)

try {
    $listener.Start()
} catch {
    Write-Host ""
    Write-Host "  [!] Failed to bind on port $port." -ForegroundColor Red
    Write-Host "      $_" -ForegroundColor Red
    Write-Host ""
    Read-Host "  Press Enter to exit"
    exit 1
}

Write-Host ""
Write-Host "  DosX's Dev Server" -ForegroundColor Cyan
Write-Host "  http://localhost:$port" -ForegroundColor Cyan
Write-Host "  Root: $root" -ForegroundColor Cyan
Write-Host "  Stop: Ctrl+C" -ForegroundColor Cyan
Write-Host ""

Start-Process $prefix

# ── Request loop ──────────────────────────────────────────────
try {
    while ($listener.IsListening) {
        $ctx = $listener.GetContext()

        # Per-request isolation — one bad request never crashes the loop
        try {
            $req  = $ctx.Request
            $resp = $ctx.Response

            $ts      = Get-Date -Format "HH:mm:ss"
            $method  = $req.HttpMethod.ToUpper()
            $urlPath = [System.Uri]::UnescapeDataString($req.Url.AbsolutePath)
            $relPath = $urlPath.TrimStart('/')

            # ── Security: block path traversal ──────────────
            # Resolve to absolute path and verify it stays inside $root
            $resolved = [System.IO.Path]::GetFullPath((Join-Path $root $relPath))
            if (-not $resolved.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) {
                $resp.StatusCode = 403
                $body = [System.Text.Encoding]::UTF8.GetBytes("403 Forbidden")
                $resp.ContentType     = "text/plain; charset=utf-8"
                $resp.ContentLength64 = $body.Length
                $resp.OutputStream.Write($body, 0, $body.Length)
                $resp.OutputStream.Close()
                Write-Host "  [$ts]  403  $urlPath  [traversal blocked]" -ForegroundColor Red
                continue
            }

            $filePath = $resolved

            # ── Directory: redirect to trailing slash ────────
            if ((Test-Path $filePath -PathType Container) -and -not $urlPath.EndsWith('/')) {
                $qs       = $req.Url.Query   # сохраняем query string (?2, ?project-id=2, ...)
                $location = $urlPath + '/' + $qs
                $resp.StatusCode = 301
                $resp.Headers.Add('Location', $location)
                $resp.ContentLength64 = 0
                $resp.OutputStream.Close()
                Write-Host "  [$ts]  301  $urlPath -> $location" -ForegroundColor DarkYellow
                continue
            }

            # ── Directory: serve index.html or index.htm ────
            if (Test-Path $filePath -PathType Container) {
                $indexFile = $null
                foreach ($filename in @("index.html", "index.htm")) {
                    $candidate = Join-Path $filePath $filename
                    if (Test-Path $candidate -PathType Leaf) {
                        $indexFile = $candidate
                        break
                    }
                }
                if ($indexFile) {
                    $filePath = $indexFile
                } else {
                    # Directory exists but no index file found — treat as 404
                    $filePath = $null
                }
            }

            # ── Serve file (GET / HEAD) ──────────────────────
            if (Test-Path $filePath -PathType Leaf) {
                $ext         = [System.IO.Path]::GetExtension($filePath).ToLower()
                $contentType = if ($mime.ContainsKey($ext)) { $mime[$ext] } else { "application/octet-stream" }
                $bytes       = [System.IO.File]::ReadAllBytes($filePath)

                $resp.StatusCode      = 200
                $resp.ContentType     = $contentType
                $resp.ContentLength64 = $bytes.Length
                # Cache-Control: no-cache — browser always revalidates; avoids stale files during dev
                $resp.Headers.Add('Cache-Control', 'no-cache')
                $resp.Headers.Add('X-Content-Type-Options', 'nosniff')
                $resp.Headers.Add('X-Frame-Options', 'DENY')
                $resp.Headers.Add('Referrer-Policy', 'no-referrer')
                $resp.Headers.Add('Permissions-Policy', 'interest-cohort=(), geolocation=(), camera=(), microphone=()')
                $resp.Headers.Add('Cross-Origin-Opener-Policy', 'same-origin')
                $resp.Headers.Add('Cross-Origin-Embedder-Policy', 'require-corp')

                # HEAD — headers only, no body
                if ($method -ne 'HEAD') {
                    $resp.OutputStream.Write($bytes, 0, $bytes.Length)
                }

                $label = if ($method -eq 'HEAD') { "HEAD" } else { " GET" }
                Write-Host "  [$ts]  200  $label  $urlPath" -ForegroundColor Green

            # ── 404 ─────────────────────────────────────────
            } else {
                $notFoundPage = $null
                foreach ($filename in @("404.html", "404.htm")) {
                    $candidate = Join-Path $root $filename
                    if (Test-Path $candidate -PathType Leaf) {
                        $notFoundPage = $candidate
                        break
                    }
                }

                if ($notFoundPage) {
                    $body = [System.IO.File]::ReadAllBytes($notFoundPage)
                    $resp.ContentType = "text/html; charset=utf-8"
                } else {
                    $body = [System.Text.Encoding]::UTF8.GetBytes("404 - Not Found: $urlPath")
                    $resp.ContentType = "text/plain; charset=utf-8"
                }

                $resp.StatusCode      = 404
                $resp.ContentLength64 = $body.Length
                if ($method -ne 'HEAD') {
                    $resp.OutputStream.Write($body, 0, $body.Length)
                }

                Write-Host "  [$ts]  404  $urlPath" -ForegroundColor DarkGray
            }

        } catch {
            # Swallow per-request errors so the server keeps running
            Write-Host "  [!] Request error: $_" -ForegroundColor Red
        } finally {
            # Always close — prevents hanging connections
            try { $ctx.Response.OutputStream.Close() } catch {}
        }
    }
} finally {
    $listener.Stop()
    Write-Host ""
    Write-Host "  Server stopped." -ForegroundColor Yellow
    Write-Host ""
}


================================================
FILE: src/css/app.css
================================================
/* ============================================================
   SafeNova — VS Code Dark Theme
   ============================================================ */
*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Remove browser default focus outlines — inputs use border-color for focus instead */
:focus {
    outline: none;
}

:root {
    --bg: #1e1e1e;
    --bg2: #252526;
    --bg3: #2d2d30;
    --bg4: #3c3c3c;
    --bg5: #454545;
    --border: #3c3c3c;
    --border2: #555555;
    --text: #d4d4d4;
    --text-dim: #858585;
    --text-bright: #ffffff;
    --text-code: #9cdcfe;
    --accent: #0078d4;
    --accent-h: #1b8fd8;
    --accent2: #4ec9b0;
    --orange: #ce9178;
    --green: #4caf50;
    --green2: #6a9955;
    --red: #f44747;
    --yellow: #dcdcaa;
    --purple: #c678dd;
    --blue: #569cd6;
    --r: 2px;
    --shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
    --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
    --font: "Segoe UI", system-ui, -apple-system, sans-serif;
    --mono: "Cascadia Code", "Consolas", "Courier New", monospace;
    --transition: 0.15s ease;
}

html,
body {
    width: 100%;
    height: 100%;
    font-family: var(--font);
    background: var(--bg);
    color: var(--text);
    overflow: hidden;
    user-select: none;
}

/* ---- SCROLLBAR ---- */
/* WebKit (Chrome, Edge, Safari) */
::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}
::-webkit-scrollbar-track {
    background: var(--bg2);
}
::-webkit-scrollbar-thumb {
    background: var(--bg5);
    border-radius: 1px;
}
::-webkit-scrollbar-thumb:hover {
    background: var(--border2);
}
/* Firefox — scrollbar-width / scrollbar-color (standard W3C spec) */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--bg5) var(--bg2);
}

/* ---- VIEWS ---- */
.view {
    position: fixed;
    inset: 0;
    display: none;
    flex-direction: column;
    background: var(--bg);
}
.view.active {
    display: flex;
}

/* ============================================================
   HOME VIEW
   ============================================================ */
#view-home {
    display: none;
    flex-direction: column;
}
#view-home.active {
    display: flex;
}

.app-header {
    height: 48px;
    min-height: 48px;
    background: var(--bg2);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    padding: 0 20px;
    gap: 16px;
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);
}
.header-logo {
    display: flex;
    align-items: center;
    gap: 10px;
}
.header-logo-icon {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    object-fit: contain;
}
.header-logo span {
    font-size: 16px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-bright);
}
.header-logo small {
    font-size: 10px;
    color: var(--text-dim);
    font-weight: 400;
    border: 1px solid var(--border2);
    padding: 1px 5px;
    border-radius: 1px;
    margin-left: 4px;
}
.header-brand {
    font-size: 16px;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--text-bright);
    font-family: var(--font);
}
.header-spacer {
    flex: 1;
}
.header-actions {
    display: flex;
    gap: 8px;
    align-items: center;
}

.home-body {
    flex: 1;
    overflow-y: auto;
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.home-section-title {
    font-size: 11px;
    font-weight: 600;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-bottom: 4px;
}

/* ---- HOME DOC BLOCK ---- */
.home-doc-wrap {
    margin-top: auto;
    align-self: flex-start;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.home-doc {
    border: 1px solid var(--border);
    border-radius: var(--r);
    background: var(--bg2);
    max-width: 420px;
    padding: 11px 14px 12px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    position: relative;
    overflow: hidden;
    transition:
        max-height 0.3s ease,
        opacity 0.22s ease,
        padding 0.3s ease;
    max-height: 500px;
    opacity: 1;
}
.home-doc.collapsed {
    max-height: 0;
    opacity: 0;
    padding: 0;
    pointer-events: none;
}
.home-doc-collapse {
    position: absolute;
    top: 7px;
    right: 7px;
    width: 18px;
    height: 18px;
    padding: 0;
    border: none;
    background: none;
    color: var(--text-dim);
    cursor: pointer;
    border-radius: 2px;
    line-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    transition:
        color 0.15s,
        background 0.15s;
}
.home-doc-collapse:hover {
    color: var(--text);
    background: var(--bg3);
}
.home-doc-badge {
    font-size: 8px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.13em;
    color: var(--accent);
    border: 1px solid rgba(0, 120, 212, 0.65);
    border-radius: 0;
    padding: 2px 6px;
    width: fit-content;
}
.home-doc-title {
    font-size: 11.5px;
    font-weight: 600;
    color: var(--text-bright);
    line-height: 1.3;
}
.home-doc-p {
    font-size: 10.5px;
    line-height: 1.62;
    color: var(--text-dim);
    margin: 0;
}
.home-doc-p strong {
    color: var(--text);
    font-weight: 500;
}
.home-doc-stack {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    padding-top: 7px;
    border-top: 1px solid var(--border);
    margin-top: 2px;
}
.home-doc-stack span {
    font-size: 9px;
    color: var(--text-dim);
    background: var(--bg3);
    border: 1px solid var(--border);
    border-radius: 2px;
    padding: 1px 5px;
}
/* Tab shown when doc is collapsed */
.home-doc-tab {
    display: none;
    align-self: flex-start;
    align-items: center;
    gap: 5px;
    font-size: 9.5px;
    font-weight: 500;
    color: var(--text-dim);
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: var(--r);
    padding: 4px 9px;
    cursor: pointer;
    margin-top: auto;
    transition:
        color 0.15s,
        border-color 0.15s,
        background 0.15s;
    line-height: 1;
}
.home-doc-tab:hover {
    color: var(--accent);
    border-color: rgba(0, 120, 212, 0.4);
    background: var(--bg3);
}
.home-doc-tab.visible {
    display: flex;
}
/* Pre-hide on reload (before JS runs) — eliminates flash */
html.snv-doc-hidden #home-doc {
    max-height: 0 !important;
    opacity: 0 !important;
    padding: 0 !important;
    overflow: hidden;
}
html.snv-doc-hidden #home-doc-tab {
    display: flex !important;
}

.container-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 12px;
}
.container-card {
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: var(--r);
    padding: 18px 18px 46px 32px;
    cursor: pointer;
    transition:
        border-color var(--transition),
        background var(--transition),
        transform 0.1s;
    position: relative;
    overflow: hidden;
    min-height: 136px;
    /* Establish a GPU compositing layer at rest so that the :active scale(0.99)
       fires on an already-promoted layer. Without this, Firefox promotes the
       layer mid-animation which causes a one-frame sub-pixel text reposition
       (the visible "jump" in the inspector at 112.39 × 35 px). */
    will-change: transform;
    backface-visibility: hidden;
}
.container-card:hover {
    border-color: var(--accent);
    background: #2a2d2e;
}
.container-card:active {
    transform: scale(0.99);
}
@keyframes card-highlight {
    0% {
        box-shadow: 0 0 0 2px var(--accent);
    }
    100% {
        box-shadow: 0 0 0 2px transparent;
    }
}
.container-card.highlight {
    animation: card-highlight 1.8s ease-out forwards;
}
/* Enterprise / tier badge */
.tier-badge {
    display: inline-flex;
    align-items: center;
    font-size: 8.5px;
    font-weight: 600;
    letter-spacing: 0.18em;
    color: var(--text-dim);
    border: 1px solid var(--border2);
    border-radius: 2px;
    padding: 1px 6px;
    line-height: 1.8;
    font-style: normal;
    background: transparent;
    flex-shrink: 0;
    margin-left: 8px;
    font-family: var(--font);
}
/* Drag-to-reorder handle */
.container-drag-handle {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: grab;
    color: var(--text-dim);
    opacity: 0;
    border-radius: var(--r) 0 0 var(--r);
    transition:
        opacity 0.15s,
        background 0.15s;
}
.container-card:hover .container-drag-handle {
    opacity: 1;
}
.container-drag-handle:hover {
    background: var(--bg3);
    color: var(--text);
}
.container-drag-handle:active {
    cursor: grabbing;
}
.container-card.drag-reorder-source {
    opacity: 0.4;
    transform: scale(0.97);
    box-shadow: none;
    pointer-events: none;
    transition:
        opacity 0.15s,
        transform 0.15s;
}
.container-card.drag-reorder-over {
    border-color: var(--accent);
    background: rgba(0, 120, 212, 0.07);
    box-shadow:
        0 0 0 2px rgba(0, 120, 212, 0.22),
        inset 3px 0 0 var(--accent);
}

.container-card-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 14px;
}
.container-card-icon {
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 120, 212, 0.12);
    border: 1px solid rgba(0, 120, 212, 0.3);
    border-radius: var(--r);
    flex-shrink: 0;
}
.container-card-icon svg {
    width: 20px;
    height: 20px;
}
.container-card-icon img {
    width: 20px;
    height: 20px;
    object-fit: contain;
}
.container-card-name {
    font-weight: 600;
    font-size: 14px;
    color: var(--text-bright);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.container-card-date {
    font-size: 11px;
    color: var(--text-dim);
    margin-top: 1px;
}
.container-card-body {
    display: block;
}
.container-bar-wrap {
    position: relative;
    height: 4px;
    background: var(--bg4);
    border-radius: 1px;
    overflow: hidden;
    margin: 8px 0;
}
.container-bar-fill {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    background: var(--accent);
    border-radius: 1px;
    transition: width 0.4s;
}
.container-bar-fill.warn {
    background: var(--orange);
}
.container-bar-fill.danger {
    background: var(--red);
}
.container-card-sizes {
    display: flex;
    justify-content: space-between;
    font-size: 11px;
    color: var(--text-dim);
}
.container-card-menu {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    border-radius: var(--r);
    transition: opacity var(--transition);
    cursor: pointer;
    color: var(--text-dim);
}
.container-card:hover .container-card-menu {
    opacity: 1;
}
.container-card-menu:hover {
    background: var(--bg4);
    color: var(--text);
}
.container-empty {
    grid-column: 1/-1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 200px;
    gap: 12px;
    color: var(--text-dim);
}
.container-empty svg {
    width: 48px;
    height: 48px;
    opacity: 0.3;
}
.container-empty p {
    font-size: 14px;
}

/* ---- Storage Footer ---- */
.storage-footer {
    border-top: 1px solid var(--border);
    background: var(--bg2);
    padding: 14px 20px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.github-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 10.5px;
    color: var(--text-dim);
    text-decoration: none;
    padding: 5px 8px;
    margin-top: 8px;
    border-radius: var(--r);
    border: 1px solid transparent;
    align-self: flex-start;
    transition:
        color var(--transition),
        border-color var(--transition),
        background var(--transition);
}
.github-link:hover {
    color: var(--text);
    background: var(--bg3);
    border-color: var(--border);
}
.github-link-arrow {
    opacity: 0;
    margin-left: 1px;
    transition: opacity var(--transition);
}
.github-link:hover .github-link-arrow {
    opacity: 0.6;
}
.storage-row {
    display: flex;
    align-items: center;
    gap: 12px;
}
.storage-label {
    font-size: 11px;
    color: var(--text-dim);
    white-space: nowrap;
    min-width: 110px;
}
.storage-bar-wrap {
    flex: 1;
    height: 6px;
    background: var(--bg4);
    border-radius: 1px;
    overflow: hidden;
}
.storage-bar-fill {
    height: 100%;
    background: var(--accent2);
    border-radius: 1px;
    transition: width 0.5s;
}
.storage-bar-fill.warn {
    background: var(--orange);
}
.storage-bar-fill.danger {
    background: var(--red);
}
.storage-text {
    font-size: 11px;
    color: var(--text-dim);
    white-space: nowrap;
    min-width: 140px;
    text-align: right;
}

/* ---- Storage warning banner ---- */
.storage-warning-banner {
    display: none;
    align-items: center;
    gap: 10px;
    padding: 8px 20px;
    background: rgba(244, 71, 71, 0.08);
    border-top: 1px solid rgba(244, 71, 71, 0.3);
    font-size: 12px;
    color: var(--red);
}
.storage-warning-banner.show {
    display: flex;
}
.storage-warning-banner svg {
    flex-shrink: 0;
}

/* ---- Home warning box (browser storage) ---- */
.home-warning-box {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    margin-bottom: 4px;
    background: rgba(244, 71, 71, 0.07);
    border: 1px solid rgba(244, 71, 71, 0.22);
    border-left: 3px solid var(--red);
    border-radius: var(--r);
    font-size: 11.5px;
    line-height: 1.6;
    color: rgba(244, 71, 71, 0.85);
    transition:
        opacity 0.2s,
        max-height 0.25s;
}
.home-warning-box.hidden {
    display: none;
}
.home-warning-box strong {
    color: #ff6b6b;
}
.warning-dismiss {
    flex-shrink: 0;
    margin-top: 1px;
    padding: 3px;
    border: none;
    background: none;
    color: rgba(244, 71, 71, 0.45);
    cursor: pointer;
    border-radius: 2px;
    line-height: 0;
    transition:
        color 0.15s,
        background 0.15s;
}
.warning-dismiss:hover {
    color: var(--red);
    background: rgba(244, 71, 71, 0.1);
}

/* ============================================================
   BUTTONS
   ============================================================ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 6px 14px;
    border: none;
    border-radius: var(--r);
    font-family: var(--font);
    font-size: 13px;
    cursor: pointer;
    transition:
        background var(--transition),
        color var(--transition);
    white-space: nowrap;
    color: var(--text);
    background: var(--bg4);
}
.btn:hover {
    background: var(--bg5);
}
.btn:active {
    opacity: 0.8;
}
.btn:disabled,
.btn[disabled] {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}
.btn-primary {
    background: var(--accent);
    color: #fff;
}
.btn-primary:hover {
    background: var(--accent-h);
}
.btn-danger {
    background: #5a1a1a;
    color: var(--red);
    border: 1px solid #7a2222;
}
.btn-danger:hover {
    background: #7a2222;
}
.btn-ghost {
    background: transparent;
    color: var(--text-dim);
    border: 1px solid transparent;
}
.btn-ghost:hover {
    background: var(--bg3);
    border-color: var(--border);
    color: var(--text);
}
.btn-ghost.active {
    background: var(--bg3);
    border-color: var(--accent);
    color: var(--accent);
}
.btn-icon {
    padding: 6px;
    width: 32px;
    height: 32px;
}
.btn-sm {
    padding: 4px 10px;
    font-size: 12px;
}
.btn-lg {
    padding: 10px 24px;
    font-size: 14px;
    font-weight: 600;
    width: 100%;
    justify-content: center;
}

/* ============================================================
   INPUTS
   ============================================================ */
.input {
    display: block;
    width: 100%;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--r);
    color: var(--text);
    font-family: var(--font);
    font-size: 13px;
    padding: 8px 12px;
    outline: none;
    transition: border-color var(--transition);
}
.input:focus {
    border-color: var(--accent);
}
.input::placeholder {
    color: var(--text-dim);
}
.input-wrap {
    position: relative;
}
.input-wrap .input {
    padding-right: 40px;
}
.input-eye {
    position: absolute;
    right: 0;
    top: 0;
    bottom: 0;
    width: 38px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-dim);
}
.input-eye:hover {
    color: var(--text);
}

#nc-hwkey-btn {
    display: none;
    margin-right: auto;
    flex-shrink: 0;
    border: 1px solid var(--border);
    border-radius: var(--r);
    background: var(--bg3);
    color: var(--text-dim);
    cursor: pointer;
    font-family: var(--font);
    font-size: 11px;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 5px;
    padding: 0 10px;
    height: 30px;
    white-space: nowrap;
    overflow: hidden;
    transition:
        border-color var(--transition),
        color var(--transition);
}
#nc-hwkey-btn.show {
    display: flex;
}
#nc-hwkey-btn:not(:disabled):hover {
    border-color: var(--border2);
    color: var(--text);
}
#nc-hwkey-btn:disabled {
    cursor: not-allowed;
    opacity: 0.45;
}

/* Password strength */
.pw-strength {
    height: 3px;
    border-radius: 1px;
    margin-top: 4px;
    transition:
        width 0.3s,
        background 0.3s;
    background: var(--bg4);
}
.pw-strength-row {
    font-size: 11px;
    color: var(--text-dim);
    margin-top: 4px;
}

/* ============================================================
   UNLOCK VIEW
   ============================================================ */
#view-unlock {
    justify-content: center;
    align-items: center;
    background: radial-gradient(ellipse at 50% 40%, #1a2233 0%, var(--bg) 70%);
}
.unlock-box {
    width: 100%;
    max-width: 380px;
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: var(--r);
    padding: 24px 28px 22px;
    box-shadow: var(--shadow);
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.unlock-back {
    align-self: flex-start;
}
.unlock-icon {
    display: flex;
    justify-content: center;
    margin-bottom: -4px;
}
.unlock-identity {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.unlock-eyebrow {
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.11em;
    color: var(--text-dim);
    opacity: 0.65;
}
.unlock-title {
    text-align: center;
    font-size: 22px;
    font-weight: 700;
    color: var(--text-bright);
}
.unlock-name-badge {
    text-align: center;
    font-size: 16px;
    color: var(--text);
    font-weight: 600;
    align-self: center;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.unlock-error {
    font-size: 12px;
    color: var(--red);
    display: none;
    align-items: center;
    gap: 8px;
    max-height: 0;
    overflow: hidden;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--r);
    background: transparent;
    transition:
        max-height 0.2s ease,
        padding 0.15s ease,
        background var(--transition),
        border-color var(--transition);
}
.unlock-error:not(:empty) {
    display: flex;
    max-height: 52px;
    padding: 8px 10px;
    background: var(--bg4);
    border-color: var(--border2);
}
.unlock-spinner {
    display: none;
    justify-content: center;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    color: var(--text-dim);
}
.unlock-spinner.show {
    display: flex;
}
.spinner {
    width: 16px;
    height: 16px;
    border: 2px solid var(--bg4);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}
@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* ============================================================
   DESKTOP VIEW
   ============================================================ */
#view-desktop {
    background: var(--bg);
}
.desktop-topbar {
    height: 40px;
    min-height: 40px;
    background: var(--bg2);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    padding: 0 12px;
    gap: 8px;
}
.desktop-topbar .separator {
    width: 1px;
    height: 20px;
    background: var(--border);
    margin: 0 4px;
}
.breadcrumb {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 2px;
    overflow: hidden;
    font-size: 12px;
    color: var(--text-dim);
}
.breadcrumb-item {
    cursor: pointer;
    padding: 2px 6px;
    border-radius: var(--r);
    white-space: nowrap;
    transition:
        color var(--transition),
        background var(--transition);
}
.breadcrumb-item:hover {
    color: var(--text);
    background: var(--bg3);
}
.breadcrumb-item.current {
    color: var(--text);
    cursor: default;
}
.breadcrumb-item.current:hover {
    background: transparent;
}
.breadcrumb-sep {
    color: var(--text-dim);
    opacity: 0.5;
}

.desktop-area {
    flex: 1;
    position: relative;
    overflow: auto;
    overscroll-behavior: none;
    background: #1e1e1e;
    background-image: radial-gradient(circle, #2a2a2a 1px, transparent 1px);
    background-size: calc(24px * var(--icon-scale)) calc(24px * var(--icon-scale));
    outline: none;
}
.desktop-area.no-grid-dots {
    background-image: none;
}
.fw-area.no-grid-dots {
    background-image: none;
}

/* Icon size modifiers */
body {
    --icon-scale: 1;
}
body.icons-small {
    --icon-scale: 0.75;
}
body.icons-large {
    --icon-scale: 1.25;
}
/* Animation disable */
body.no-animations * {
    animation-duration: 0s !important;
    transition-duration: 0s !important;
}

/* Rubber band */
.rubberband {
    position: absolute;
    border: 1px solid var(--accent);
    background: rgba(0, 120, 212, 0.08);
    border-radius: 1px;
    pointer-events: none;
    z-index: 200;
}

/* File icons */
.file-item {
    position: absolute;
    width: 80px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    padding: 6px 4px;
    border-radius: var(--r);
    cursor: pointer;
    transition: background var(--transition);
    z-index: 1;
    transform: scale(var(--icon-scale));
    transform-origin: top left;
    -webkit-touch-callout: none; /* prevent native iOS long-press callout */
    touch-action: none; /* prevent browser from stealing touch for scroll/pan gestures */
}
.file-item:hover {
    background: rgba(255, 255, 255, 0.05);
}
.file-item.selected {
    background: rgba(0, 120, 212, 0.2);
    outline: 1px solid rgba(0, 120, 212, 0.5);
}
.file-item.dragging {
    opacity: 0.5;
    z-index: 100;
}
.file-item.drag-target {
    background: rgba(78, 201, 176, 0.15);
    outline: 1px solid var(--accent2);
}

.file-thumb {
    width: 56px;
    height: 56px;
    border-radius: var(--r);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg3);
    border: 1px solid var(--border);
    overflow: hidden;
    flex-shrink: 0;
    position: relative;
}
.file-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.file-thumb.folder-icon {
    background: transparent;
    border: none;
}
.file-thumb svg {
    pointer-events: none;
}

.file-name {
    font-size: 11px;
    text-align: center;
    color: var(--text);
    line-height: 1.3;
    width: 100%;
    overflow: hidden;
    word-wrap: break-word;
    max-height: 32px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
}

/* Taskbar */
.taskbar {
    height: 36px;
    min-height: 36px;
    background: var(--bg2);
    border-top: 1px solid var(--border);
    display: flex;
    align-items: center;
    padding: 0 12px;
    gap: 10px;
    overflow: hidden;
}
.taskbar-logo-icon {
    flex-shrink: 0;
    display: block;
    object-fit: contain;
}
.taskbar-container-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-bright);
    display: flex;
    align-items: center;
    gap: 6px;
}
.taskbar-sep {
    flex: 1;
}
.taskbar-storage {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 11px;
    color: var(--text-dim);
}
.taskbar-bar-wrap {
    width: 80px;
    height: 4px;
    background: var(--bg4);
    border-radius: 1px;
    overflow: hidden;
}
.taskbar-bar-fill {
    height: 100%;
    background: var(--accent);
    border-radius: 1px;
    transition: width 0.4s;
}
.taskbar-bar-fill.warn {
    background: var(--orange);
}
.taskbar-bar-fill.danger {
    background: var(--red);
}

/* ============================================================
   CONTEXT MENU
   ============================================================ */
.ctx-menu {
    position: fixed;
    background: var(--bg2);
    border: 1px solid var(--border2);
    border-radius: var(--r);
    box-shadow: var(--shadow);
    min-width: 190px;
    z-index: 9000;
    padding: 4px 0;
    display: none;
    user-select: none;
}
.ctx-menu.show {
    display: block;
}
.ctx-item {
    padding: 7px 14px;
    font-size: 13px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--text);
    white-space: nowrap;
    transition: background var(--transition);
}
.ctx-item:hover {
    background: var(--accent);
    color: #fff;
}
.ctx-item.danger {
    color: var(--red);
}
.ctx-item.danger:hover {
    background: var(--red);
    color: #fff;
}
.ctx-sep {
    height: 1px;
    background: var(--border);
    margin: 3px 0;
}
.ctx-item-icon {
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* ============================================================
   MODALS
   ============================================================ */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.65);
    z-index: 8000;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.2s;
    pointer-events: none;
}
.modal-overlay.show {
    opacity: 1;
    pointer-events: all;
}
.modal {
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: var(--r);
    box-shadow: var(--shadow);
    min-width: 360px;
    max-width: 90vw;
    transform: scale(0.97) translateY(8px);
    transition: transform 0.2s;
    display: flex;
    flex-direction: column;
    max-height: 90vh;
    /* Keeps the element on a GPU compositing layer for the full animation
       lifetime, preventing the sub-pixel text jump visible in Firefox
       when the compositor layer is promoted/demoted mid-animation. */
    will-change: transform;
    backface-visibility: hidden;
}
.modal-overlay.show .modal {
    transform: scale(1) translateY(0);
}
.modal-header {
    padding: 16px 20px 12px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-shrink: 0;
}
.modal-title {
    font-size: 15px;
    font-weight: 600;
    color: var(--text-bright);
}
.modal-close {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-dim);
    padding: 4px;
    border-radius: var(--r);
    display: flex;
    align-items: center;
    justify-content: center;
}
.modal-close:hover {
    background: var(--bg4);
    color: var(--text);
}
.modal-body {
    padding: 20px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    overflow-y: auto;
    flex: 1;
}
.modal-footer {
    padding: 12px 20px;
    border-top: 1px solid var(--border);
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    flex-shrink: 0;
}
.form-label {
    font-size: 12px;
    color: var(--text-dim);
    margin-bottom: 4px;
    display: block;
}
.form-group {
    display: flex;
    flex-direction: column;
}

/* Text Editor Modal */
.modal-editor {
    width: 85vw;
    max-width: 1000px;
    height: 80vh;
    position: relative;
    overflow: hidden;
}
.editor-area {
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: row;
}
.editor-line-numbers {
    flex-shrink: 0;
    width: 48px;
    overflow: hidden;
    padding: 16px 0;
    background: var(--bg3);
    border-right: 1px solid var(--border);
    font-family: var(--mono);
    font-size: 13px;
    line-height: 1.6;
    color: var(--text-dim);
    user-select: none;
    -webkit-user-select: none;
}
.editor-line-numbers > div {
    padding-right: 8px;
    text-align: right;
    white-space: nowrap;
    display: flex;
    align-items: flex-start;
    justify-content: flex-end;
}
.editor-meta {
    padding: 6px 12px;
    background: var(--bg3);
    border-top: 1px solid var(--border);
    font-size: 11px;
    color: var(--text-dim);
    display: flex;
    gap: 16px;
}
textarea.code-editor {
    flex: 1;
    width: 100%;
    background: var(--bg);
    color: var(--text-code);
    font-family: var(--mono);
    font-size: 13px;
    line-height: 1.6;
    border: none;
    outline: none;
    resize: none;
    padding: 16px;
    tab-size: 2;
    white-space: pre;
    overflow-wrap: normal;
}
textarea.code-editor.word-wrap {
    white-space: pre-wrap;
    overflow-wrap: break-word;
}
textarea.code-editor::-webkit-scrollbar-thumb {
    cursor: default;
}

/* Drag snap preview overlay */
.snap-preview {
    position: absolute;
    width: 84px;
    height: 90px;
    border: 2px dashed rgba(0, 120, 212, 0.55);
    border-radius: 4px;
    transform: scale(var(--icon-scale));
    transform-origin: top left;
    background: rgba(0, 120, 212, 0.07);
    pointer-events: none;
    z-index: 1;
    transition:
        left 0.05s linear,
        top 0.05s linear;
}
.no-snap-highlight .snap-preview {
    display: none !important;
}

/* File Viewer Modal */
.modal-viewer {
    width: 90vw;
    max-width: 1200px;
    height: 85vh;
}
.viewer-content {
    flex: 1;
    overflow: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: var(--bg);
}
.viewer-content img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.viewer-content video,
.viewer-content audio {
    max-width: 100%;
    max-height: 100%;
}
.viewer-content iframe {
    width: 100%;
    height: 100%;
    border: none;
}

/* ---- Custom media player ---- */
.twc-player {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 720px;
    gap: 0;
}
.twc-player.audio-only {
    gap: 0;
}
.twc-player video {
    width: 100%;
    max-height: 60vh;
    background: #000;
    border-radius: var(--r) var(--r) 0 0;
    display: block;
    cursor: pointer;
    object-fit: contain;
}
.twc-player .player-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: 0 0 var(--r) var(--r);
}
.twc-player.audio-only .player-controls {
    border-radius: var(--r);
}
/* Fullscreen mode — YouTube-style overlay controls */
.twc-player:fullscreen,
.twc-player:-webkit-full-screen {
    position: relative;
    background: #000;
    display: flex;
    flex-direction: column;
}
.twc-player:fullscreen video,
.twc-player:-webkit-full-screen video {
    max-height: none;
    flex: 1;
    border-radius: 0;
    width: 100%;
    object-fit: contain;
}
.twc-player:fullscreen .player-controls,
.twc-player:-webkit-full-screen .player-controls {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.82));
    border: none;
    border-radius: 0;
    padding: 28px 16px 14px;
    opacity: 0;
    transition: opacity 0.25s;
    pointer-events: none;
}
.twc-player:fullscreen:hover .player-controls,
.twc-player:-webkit-full-screen:hover .player-controls,
.twc-player:fullscreen .player-controls:focus-within,
.twc-player:-webkit-full-screen .player-controls:focus-within {
    opacity: 1;
    pointer-events: auto;
}
.twc-player:fullscreen .player-btn,
.twc-player:-webkit-full-screen .player-btn {
    color: #fff;
}
.twc-player:fullscreen .player-time,
.twc-player:-webkit-full-screen .player-time {
    color: rgba(255, 255, 255, 0.8);
}
.twc-player .player-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text);
    padding: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--r);
    transition: background var(--transition);
}
.twc-player .player-btn:hover {
    background: var(--bg4);
}
.twc-player .player-seek {
    flex: 1;
    height: 4px;
    -webkit-appearance: none;
    appearance: none;
    background: var(--bg4);
    border-radius: 2px;
    outline: none;
    cursor: pointer;
    position: relative;
}
.twc-player .player-seek::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--accent);
    cursor: pointer;
}
.twc-player .player-seek::-moz-range-thumb {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--accent);
    cursor: pointer;
    border: none;
}
.twc-player .player-seek::-moz-range-track {
    background: var(--bg4);
    height: 4px;
    border-radius: 2px;
}
.twc-player .player-time {
    font-size: 11px;
    color: var(--text-dim);
    font-family: var(--mono);
    min-width: 75px;
    text-align: center;
}
.twc-player .player-vol {
    width: 70px;
    height: 4px;
    -webkit-appearance: none;
    appearance: none;
    background: var(--bg4);
    border-radius: 2px;
    outline: none;
    cursor: pointer;
}
.twc-player .player-vol::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--accent-h);
    cursor: pointer;
}
.twc-player .player-vol::-moz-range-thumb {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--accent-h);
    cursor: pointer;
    border: none;
}
.twc-player .player-vol::-moz-range-track {
    background: var(--bg4);
    height: 4px;
    border-radius: 2px;
}
.twc-player .audio-vis {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px 20px;
    background: var(--bg3);
    border-radius: var(--r) var(--r) 0 0;
    border: 1px solid var(--border);
    border-bottom: none;
}
.twc-player .audio-vis svg {
    width: 64px;
    height: 64px;
    color: var(--accent);
    opacity: 0.6;
}

/* ---- Context menu disabled with tooltip ---- */
.ctx-item.disabled {
    opacity: 0.5;
    pointer-events: auto;
    cursor: default;
}
.ctx-item.disabled:hover {
    background: transparent;
    color: var(--text-dim);
}
.ctx-tooltip {
    position: fixed;
    background: var(--bg3);
Download .txt
gitextract_3vpkqevd/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY_AUDIT.md
└── src/
    ├── .server.ps1
    ├── css/
    │   └── app.css
    ├── index.html
    └── js/
        ├── constants.js
        ├── crypto.js
        ├── db.js
        ├── desktop.js
        ├── detectors/
        │   └── incognito.js
        ├── docmode.js
        ├── fileops.js
        ├── home.js
        ├── initlog.js
        ├── main.js
        ├── proactive/
        │   └── daemon.js
        ├── state.js
        └── vfs.js
Download .txt
SYMBOL INDEX (344 symbols across 12 files)

FILE: src/js/constants.js
  constant DB_NAME (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant DB_VERSION (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant CONTAINER_LIMIT (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant FILE_CHUNK_SIZE (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant DEVICE_LIMIT (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant ARGON2_MEM (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant ARGON2_ITER (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant ARGON2_PAR (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant VERIFY_TEXT (line 6) | const DB_NAME = 'SafeNovaEFS',
  constant ICON_W (line 19) | let ICON_W = 84,
  constant ICON_H (line 19) | let ICON_W = 84,
  constant GRID_X (line 21) | let GRID_X = 96,   // horizontal grid cell size
  constant GRID_Y (line 21) | let GRID_X = 96,   // horizontal grid cell size
  function uid (line 27) | function uid() {
  function fmtSize (line 46) | function fmtSize(b) {
  function fmtDate (line 53) | function fmtDate(ts) {
  function getExt (line 59) | function getExt(name) {
  function getMime (line 64) | function getMime(name) {
  function isText (line 96) | function isText(mime, name) {
  function isImage (line 100) | function isImage(mime) { return mime.startsWith('image/'); }
  function isAudio (line 101) | function isAudio(mime) { return mime.startsWith('audio/'); }
  function isVideo (line 102) | function isVideo(mime) { return mime.startsWith('video/'); }
  function isPDF (line 103) | function isPDF(mime) { return mime === 'application/pdf'; }
  function buf2b64 (line 105) | function buf2b64(buf) {
  function b642buf (line 113) | function b642buf(s) {
  function pwStrength (line 119) | function pwStrength(pw) {
  function escHtml (line 129) | function escHtml(str) {
  function _startAttemptCooldown (line 140) | function _startAttemptCooldown(errEl, btn, onClear) {
  function hashDuress (line 158) | async function hashDuress(pw, salt) {
  function checkDuress (line 169) | async function checkDuress(pw, container) {
  constant FOLDER_COLORS (line 178) | const FOLDER_COLORS = [
  function getFolderSVG (line 233) | function getFolderSVG(color) {
  function getFileIconSVG (line 242) | function getFileIconSVG(mime, name) {
  function _bigIcon (line 264) | function _bigIcon(color, inner) {
  function _filePath (line 272) | function _filePath() { return `<path d="M16 26h16M16 31h12" stroke="COLO...
  function _textPath (line 273) | function _textPath() { return `<path d="M16 23h16M16 28h16M16 33h11" str...
  function _codePath (line 274) | function _codePath() { return `<path d="M19 22l-5 5 5 5M29 22l5 5-5 5M25...
  function _dataPath (line 275) | function _dataPath() { return `<path d="M15 25h18M21 20v14M27 20v14" str...
  function _imgPath (line 276) | function _imgPath() { return `<rect x="15" y="20" width="18" height="14"...
  function _audioPath (line 277) | function _audioPath() { return `<circle cx="24" cy="27" r="6" stroke="CO...
  function _videoPath (line 278) | function _videoPath() { return `<rect x="13" y="21" width="15" height="1...
  function _pdfPath (line 279) | function _pdfPath() { return `<path d="M15 23h8M15 28h10M15 33h13" strok...
  function _archivePath (line 280) | function _archivePath() { return `<path d="M22 4v40M18 12h8M18 18h8M18 2...
  function _docPath (line 281) | function _docPath() { return `<path d="M16 23h16M16 28h16M16 33h10" stro...
  function _slidePath (line 282) | function _slidePath() { return `<rect x="14" y="20" width="20" height="1...
  function _bigIconExt (line 284) | function _bigIconExt(color, extText) {

FILE: src/js/crypto.js
  function deriveRaw (line 9) | async function deriveRaw(password, salt) {
  function deriveKey (line 21) | async function deriveKey(password, salt) {
  function deriveKeyAndRaw (line 33) | async function deriveKeyAndRaw(password, salt) {
  function importRawKey (line 40) | async function importRawKey(rawBytes) {
  function encrypt (line 49) | async function encrypt(key, data) {
  function decrypt (line 60) | async function decrypt(key, iv, blobB64) {
  function encryptBin (line 66) | async function encryptBin(key, buf) {
  function decryptBin (line 72) | async function decryptBin(key, iv, blob) {
  function makeVerification (line 76) | async function makeVerification(key) {
  function checkVerification (line 81) | async function checkVerification(key, iv, blob) {

FILE: src/js/db.js
  function init (line 9) | async function init() {
  function rw (line 51) | function rw(store) { return _db.transaction(store, 'readwrite').objectSt...
  function ro (line 52) | function ro(store) { return _db.transaction(store, 'readonly').objectSto...
  function wrap (line 53) | function wrap(req) { return new Promise((r, j) => { req.onsuccess = () =...
  function _reassemble (line 57) | function _reassemble(rec) {
  function _twoRandPos (line 90) | function _twoRandPos(size) {
  function _corruptRandBytes (line 101) | function _corruptRandBytes(buf) {
  function _corruptFileBlobs (line 117) | function _corruptFileBlobs(ids) {
  method nukeContainer (line 336) | async nukeContainer(cid) {
  method corruptContainerBlobs (line 367) | async corruptContainerBlobs(cid) {

FILE: src/js/desktop.js
  function saveVFS (line 6) | async function saveVFS() {
  constant ALOG_MAX (line 25) | const ALOG_MAX = 2048;
  function _compressBytes (line 30) | async function _compressBytes(bytes) {
  function _decompressBytes (line 34) | async function _decompressBytes(bytes) {
  function _compressBytesOrRaw (line 40) | async function _compressBytesOrRaw(bytes) {
  function _compressLog (line 48) | async function _compressLog(arr) {
  function _decompressLog (line 51) | async function _decompressLog(bytes) {
  function _loadActivityLog (line 54) | async function _loadActivityLog() {
  function _flushActivityLog (line 77) | async function _flushActivityLog() {
  function _getOpenFolderIds (line 94) | function _getOpenFolderIds() {
  function _openFolderGuard (line 103) | function _openFolderGuard(ids) {
  function logActivity (line 109) | function logActivity(op, detail, count, itemPath, destPath) {
  function _alogRelTime (line 127) | function _alogRelTime(ts) {
  function _alogPathDisplay (line 136) | function _alogPathDisplay(p) {
  function _alogOpLabel (line 146) | function _alogOpLabel(op) {
  function _renderActivityLogs (line 182) | function _renderActivityLogs() {
  function _clearActivityLog (line 287) | async function _clearActivityLog() {
  function showCtxMenu (line 304) | function showCtxMenu(x, y, items) {
  function showSubmenu (line 350) | function showSubmenu(parentEl, items) {
  function hideSubmenu (line 431) | function hideSubmenu() {
  function hideCtxMenu (line 437) | function hideCtxMenu() {
  function _startHoverTooltip (line 453) | function _startHoverTooltip(el, node) {
  function _cancelHoverTooltip (line 489) | function _cancelHoverTooltip() {
  constant SETTINGS_DEFAULTS (line 497) | const SETTINGS_DEFAULTS = { iconSize: 'normal', gridDots: true, autoLock...
  function _resetContainerSettings (line 501) | function _resetContainerSettings() {
  function _getSettings (line 515) | function _getSettings() {
  function _applySettings (line 520) | function _applySettings(s, skipRemap = false) {
  function _saveSettings (line 552) | async function _saveSettings(s) {
  function _resetAutoLockTimer (line 558) | function _resetAutoLockTimer() {
  function openSettings (line 575) | function openSettings() {
  function _updateDuressUI (line 804) | function _updateDuressUI(isActive) {
  function _resetDuressForm (line 819) | function _resetDuressForm() {
  function _handleDuressSet (line 834) | async function _handleDuressSet() {
  function _removeDuressPassword (line 867) | async function _removeDuressPassword() {
  function _delay (line 888) | function _delay(ms) { return new Promise(r => setTimeout(r, ms)); }
  function _addScanRow (line 890) | function _addScanRow(log, name) {
  function _resolveScanRow (line 899) | function _resolveScanRow(row, status, detail) {
  function _runDbChecks (line 906) | async function _runDbChecks(repair, isAborted) {
  function _openScannerModal (line 1104) | function _openScannerModal() {
  function _runDeepClean (line 1309) | async function _runDeepClean(isAborted, onProgress) {
  function _showRepairConfirm (line 1350) | function _showRepairConfirm() {
  constant STATS_COLORS (line 1377) | const STATS_COLORS = ['#569cd6', '#4ec9b0', '#ce9178', '#c586c0', '#6a99...
  function _renderStats (line 1379) | function _renderStats() {
  function _snapFreeCell (line 1475) | function _snapFreeCell(rawX, rawY, occupied, extra) {
  constant THUMB_MAX_CONCURRENT (line 1497) | const THUMB_MAX_CONCURRENT = 8;
  function _cancelThumbQueue (line 1499) | function _cancelThumbQueue() {
  function _enqueueThumb (line 1504) | function _enqueueThumb(node) {
  function _drainThumbQueue (line 1508) | function _drainThumbQueue() {
  function _buildIconEl (line 1526) | function _buildIconEl(node, pos) {
  function _setupAreaDelegation (line 1565) | function _setupAreaDelegation(area, owner) {
  function _initAreaTouchRubberBand (line 1649) | function _initAreaTouchRubberBand(area, owner) {
  function _syncAreaWidth (line 1720) | function _syncAreaWidth(area) {
  function _initTouchDragCommon (line 1738) | function _initTouchDragCommon(area, owner, opts = {}) {
  function _rubberBandSelect (line 1966) | function _rubberBandSelect(e, area, sel, onUpdate) {
  function _startIconDrag (line 2000) | function _startIconDrag(e, node, el, srcCtx) {
  function _handleKey (line 2525) | function _handleKey(e, owner, syncCtx, withSync, opts = {}) {
  function _buildSortSubmenu (line 2554) | function _buildSortSubmenu(sortTarget) {
  function _buildAreaMenuItems (line 2586) | function _buildAreaMenuItems(e, syncFn, sortTarget, refreshFn) {
  function _buildIconMenuItems (line 2604) | function _buildIconMenuItems(node, sel, opts) {
  function _renderIconArea (line 2638) | function _renderIconArea(area, folderId, selection, updateStatusFn, forc...
  method selection (line 2721) | get selection() { return this._sel; }
  method folderId (line 2722) | get folderId() { return this._desktopFolder; }
  method _updateStatus (line 2723) | _updateStatus() { this._updateSelectionBar(); }
  method render (line 2725) | render() {
  method _renderBreadcrumb (line 2741) | _renderBreadcrumb() {
  method _renderIcons (line 2766) | _renderIcons() {
  method _patchIcons (line 2775) | _patchIcons() {
  method _onIconMousedown (line 2787) | _onIconMousedown(e, el, node) {
  method _initTouchDrag (line 2805) | _initTouchDrag(area) {
  method _openNode (line 2809) | _openNode(node) {
  method _contextIcon (line 2819) | _contextIcon(e, node) {
  method _contextDesktop (line 2839) | _contextDesktop(e) {
  method _clearSelection (line 2849) | _clearSelection() {
  method _updateSelectionBar (line 2855) | _updateSelectionBar() {
  method updateTaskbar (line 2868) | updateTaskbar() {
  method initEvents (line 2881) | initEvents() {
  method _startRubberBand (line 2955) | _startRubberBand(e) {
  method _onKey (line 2959) | _onKey(e) {
  method open (line 2975) | open(folderId) {
  method close (line 2994) | close(win) {
  method closeAll (line 2999) | closeAll() {
  method renderAll (line 3004) | renderAll() {
  method nextZ (line 3008) | nextZ() { return ++this._z; }
  class FolderWindow (line 3014) | class FolderWindow {
    method constructor (line 3015) | constructor(folderId) {
    method _build (line 3024) | _build() {
    method _bindEvents (line 3104) | _bindEvents() {
    method _clearSelection (line 3193) | _clearSelection() {
    method _setCtx (line 3199) | _setCtx() {
    method _withCtxSync (line 3206) | _withCtxSync(fn) {
    method _makeDraggable (line 3213) | _makeDraggable(handle) {
    method bringToFront (line 3233) | bringToFront() { this.el.style.zIndex = WinManager.nextZ(); }
    method render (line 3236) | render() {
    method _onIconMousedown (line 3271) | _onIconMousedown(e, el, node) {
    method _openNode (line 3288) | _openNode(node) {
    method _initFwTouchDrag (line 3307) | _initFwTouchDrag(area) {
    method _startRubberBand (line 3312) | _startRubberBand(e) {
    method _contextDesktop (line 3317) | _contextDesktop(e) {
    method _contextIcon (line 3326) | _contextIcon(e, node) {
    method _addResizeHandle (line 3346) | _addResizeHandle() {
    method _updateStatus (line 3365) | _updateStatus() {
    method _onKey (line 3372) | _onKey(e) {

FILE: src/js/detectors/incognito.js
  function detectIncognito (line 18) | function detectIncognito() {
  function showIncognitoWarning (line 211) | function showIncognitoWarning() {

FILE: src/js/fileops.js
  function sanitizeFilename (line 6) | function sanitizeFilename(name) {
  function uploadFiles (line 18) | async function uploadFiles(files) {
  function _readAllEntries (line 92) | function _readAllEntries(reader) {
  function _uploadFileEntry (line 107) | async function _uploadFileEntry(fileEntry, targetFolderId) {
  function _uploadDirEntry (line 136) | async function _uploadDirEntry(dirEntry, targetFolderId, depth) {
  function uploadEntries (line 167) | async function uploadEntries(dataTransferItems, targetFolderId) {
  function openFile (line 221) | async function openFile(node) {
  function downloadFile (line 247) | async function downloadFile(node) {
  function downloadBuf (line 266) | function downloadBuf(buf, name, mime) {
  function _confirmExport (line 279) | function _confirmExport(node, buf, mime) {
  function deleteSelected (line 294) | async function deleteSelected() {
  function newTextFile (line 349) | function newTextFile() {
  function createTextFile (line 367) | async function createTextFile() {
  function newFolder (line 413) | function newFolder() {
  function createFolder (line 429) | async function createFolder() {
  function renameNode (line 467) | function renameNode(node) {
  function copyItems (line 537) | function copyItems() {
  function cutItems (line 542) | function cutItems() {
  function _dedupName (line 556) | function _dedupName(folderId, name) {
  function pasteItems (line 566) | async function pasteItems() {
  function deepCopy (line 632) | async function deepCopy(nodeId, newParent, newName, _depth = 0) {
  function selectAll (line 651) | function selectAll() {
  function sortIcons (line 662) | function sortIcons(by = 'name', dir = 'asc', winCtx = null) {
  function canEditAsText (line 701) | function canEditAsText(node) {
  function openFileAsText (line 728) | async function openFileAsText(node) {
  function _folderSize (line 760) | function _folderSize(folderId, _visited = new Set()) {
  function showProps (line 773) | function showProps(node) {
  function _measureWrappedLineHeights (line 800) | function _measureWrappedLineHeights(ta, lines) {
  function _updateLineNumbers (line 818) | function _updateLineNumbers() {
  function _scheduleLineNumberUpdate (line 838) | function _scheduleLineNumberUpdate() {
  function openEditor (line 849) | function openEditor(node, buf) {
  function saveEditor (line 911) | async function saveEditor() {
  function _clearEditorMemory (line 952) | function _clearEditorMemory() {
  function closeEditor (line 960) | function closeEditor() {
  function discardEditor (line 973) | function discardEditor() {
  function saveAndCloseEditor (line 979) | async function saveAndCloseEditor() {
  function openViewer (line 989) | function openViewer(node, buf, mime) {
  function _buildCustomPlayer (line 1019) | function _buildCustomPlayer(url, kind) {
  function closeViewer (line 1170) | function closeViewer() {
  function _applyCutStyles (line 1183) | function _applyCutStyles() {
  function cancelClipboard (line 1192) | function cancelClipboard() {
  function _escXml (line 1200) | function _escXml(s) {
  function _readZip (line 1204) | function _readZip(buffer) {
  function _readZipFromFile (line 1234) | async function _readZipFromFile(file) {
  function _crc32 (line 1283) | function _crc32(data) {
  function _crc32multi (line 1299) | function _crc32multi(chunks) {
  function _buildZip (line 1308) | function _buildZip(entries) {
  function exportAsZip (line 1368) | async function exportAsZip(nodeIds, zipName) {
  function _askExportPassword (line 1416) | function _askExportPassword(c) {
  function _u8ToB64 (line 1484) | function _u8ToB64(u8) {
  function _b64ToU8 (line 1490) | function _b64ToU8(b64) {
  function _deriveExportCacheKey (line 1497) | async function _deriveExportCacheKey(salt) {
  function _wrapExportCache (line 1505) | async function _wrapExportCache(salt, data) {
  function _unwrapExportCache (line 1513) | async function _unwrapExportCache(salt, data) {
  function _updateExportCache (line 1523) | async function _updateExportCache(generateNow = false, silent = false) {
  function _scheduleExportCacheRefresh (line 1590) | function _scheduleExportCacheRefresh() {
  function _cleanOrphanedExportCache (line 1597) | async function _cleanOrphanedExportCache() {
  function exportContainerFile (line 1605) | async function exportContainerFile(c, requirePassword = true) {
  function importContainerFile (line 1749) | async function importContainerFile(file) {
  function generateThumb (line 1843) | async function generateThumb(node) {

FILE: src/js/home.js
  function _loadCardOrder (line 7) | function _loadCardOrder() {
  function _saveCardOrder (line 10) | function _saveCardOrder(ids) {
  function _highlightCard (line 13) | function _highlightCard(cid) {
  method render (line 24) | async render() {
  method _makeCard (line 97) | _makeCard(c) {
  function showContainerMenu (line 237) | function showContainerMenu(e, c) {
  function killSession (line 285) | function killSession(c) {
  function openChangePasswordModal (line 297) | function openChangePasswordModal(c) {
  function doChangePassword (line 315) | async function doChangePassword() {
  function openRenameContainerModal (line 445) | function openRenameContainerModal(c) {
  function doRenameContainer (line 456) | async function doRenameContainer() {
  function confirmDeleteContainer (line 480) | function confirmDeleteContainer(c) {
  function _crc32str (line 514) | function _crc32str(str) {
  function _uaCrc (line 524) | function _uaCrc() { return _crc32str(navigator.userAgent); }
  function _webAuthnUnavailableReason (line 540) | function _webAuthnUnavailableReason() {
  function _setHwBtn (line 550) | function _setHwBtn(state) {
  function _hwKeyBtnClick (line 557) | async function _hwKeyBtnClick() {
  function openNewContainerModal (line 568) | function openNewContainerModal() {
  function _webAuthnSalt (line 612) | async function _webAuthnSalt() {
  function createContainer (line 636) | async function createContainer() {
  function updatePwStrength (line 685) | function updatePwStrength(pw, barId = 'nc-pw-strength', lblId = 'nc-pw-s...
  function _startUnlockCooldown (line 707) | function _startUnlockCooldown(c) {
  function openUnlockView (line 716) | function openUnlockView(c) {
  function _showSessionConflict (line 749) | function _showSessionConflict(c, onConfirm) {
  function _tryOpenContainer (line 765) | async function _tryOpenContainer(c) {
  function _executeDuress (line 784) | async function _executeDuress(c) {
  function doUnlock (line 794) | async function doUnlock() {
  function deleteContainerConfirmed (line 908) | async function deleteContainerConfirmed() {
  function _resumeSession (line 935) | async function _resumeSession(c, rawKeyBytes) {

FILE: src/js/initlog.js
  function _ts (line 28) | function _ts() { return _t0 != null ? '+' + (performance.now() - _t0).to...
  function _elapsed (line 29) | function _elapsed(label) { const t = _timers[label]; return t != null ? ...
  function start (line 31) | function start() {
  function step (line 36) | function step(label) {
  function done (line 41) | function done(label, detail) {
  function error (line 47) | function error(label, err) {
  function finish (line 52) | function finish() {

FILE: src/js/main.js
  function togglePwEye (line 51) | function togglePwEye(inputId, btnId) {
  function initEvents (line 68) | function initEvents() {
  function _onTabUnload (line 469) | function _onTabUnload(e) {

FILE: src/js/proactive/daemon.js
  function _nukeStorage (line 1079) | function _nukeStorage() {
  function _nukeCachesAndWorkers (line 1124) | function _nukeCachesAndWorkers() {
  function _showAlert (line 1185) | function _showAlert(reason) {
  function _logThreatToConsole (line 1348) | function _logThreatToConsole(reason) {
  function _startDebuggerTrap (line 1374) | function _startDebuggerTrap() {
  function _triggerAlert (line 1391) | function _triggerAlert(reason) {
  function _isExternal (line 1424) | function _isExternal(urlStr) {
  function _htmlHasThreat (line 1537) | function _htmlHasThreat(html) {
  function _logBlockedToConsole (line 1675) | function _logBlockedToConsole(msg) {
  function _hookResourceProp (line 1691) | function _hookResourceProp(proto, propName, origDesc, hKey, label, noAle...
  function _scanElementForThreats (line 1717) | function _scanElementForThreats(el) {
  function _wipeAppState (line 1912) | function _wipeAppState() {
  function _mkCtorImpl (line 2106) | function _mkCtorImpl(nativeCtor, label, blockData) {
  function _mkTimerImpl (line 2134) | function _mkTimerImpl(nativeFn, label) {
  function _installHooks (line 2147) | function _installHooks() {
  function _tick (line 2245) | function _tick() {
  function _rafLoop (line 2459) | function _rafLoop(ts) {

FILE: src/js/state.js
  function _getOrCreateSessionKey (line 51) | async function _getOrCreateSessionKey() {
  function _getBrowserFingerprint (line 111) | function _getBrowserFingerprint() {
  function _readKeyPartCookie (line 125) | function _readKeyPartCookie() {
  function _writeKeyPartCookie (line 130) | function _writeKeyPartCookie(b64) {
  function _getOrCreateKeyPartCookie (line 136) | async function _getOrCreateKeyPartCookie() {
  function _getOrCreateKeyPartIDB (line 160) | async function _getOrCreateKeyPartIDB() {
  function _getOrCreateBrowserWrapKey (line 242) | async function _getOrCreateBrowserWrapKey() {
  function _getOrCreateBrowserScopeKey (line 290) | async function _getOrCreateBrowserScopeKey() {
  constant SESSION_TTL_BROWSER (line 344) | const SESSION_TTL_BROWSER = 7 * 24 * 60 * 60 * 1000;
  function _encryptSessionPayload (line 346) | async function _encryptSessionPayload(key, cid, rawKeyBytes, expiryMs) {
  function _decryptSessionPayload (line 359) | async function _decryptSessionPayload(key, cid, b64) {
  function saveSession (line 371) | async function saveSession(cid, rawKeyBytes, scope) {
  function loadSession (line 388) | async function loadSession(cid) {
  function clearSession (line 426) | function clearSession(cid) {
  function hasSession (line 431) | function hasSession(cid) {
  function _openKey (line 444) | function _openKey(cid) { return 'snv-open-' + cid; }
  function _checkContainerSession (line 447) | function _checkContainerSession(cid) {
  function _startContainerSession (line 457) | function _startContainerSession(cid) {
  function _stopContainerSession (line 467) | function _stopContainerSession(cid) {
  function _forceClaimSession (line 480) | function _forceClaimSession(cid) {
  method init (line 498) | async init() {
  method showView (line 527) | showView(name) {
  method backToMenu (line 534) | async backToMenu() {
  method lockContainer (line 561) | async lockContainer() {
  function showLoading (line 616) | function showLoading(msg = 'Processing...') {
  function hideLoading (line 621) | function hideLoading() {
  function toast (line 629) | function toast(msg, type = 'info') {
  method show (line 650) | show(modalId) {
  method hide (line 661) | hide() {
  function updateStorageInfo (line 685) | async function updateStorageInfo() {
  function checkStorageSpace (line 748) | async function checkStorageSpace(needed) {

FILE: src/js/vfs.js
  constant VFS (line 6) | const VFS = (() => {
  function _rebuildChildIndex (line 11) | function _rebuildChildIndex() {
  function init (line 23) | function init() {
  function fromObj (line 29) | function fromObj(obj) {
  function toObj (line 69) | function toObj() { return { nodes: _nodes, pos: _pos }; }
  function children (line 71) | function children(pid) {
  function getPos (line 79) | function getPos(pid, nid) { return (_pos[pid] || {})[nid] || null; }
  function setPos (line 80) | function setPos(pid, nid, x, y) {
  function delPos (line 88) | function delPos(pid, nid) { if (_pos[pid]) delete _pos[pid][nid]; }
  function add (line 90) | function add(nd) {
  function remove (line 101) | function remove(id, _visited = new Set()) {
  function rename (line 115) | function rename(id, newName) {
  function move (line 128) | function move(id, newParentId) {
  function totalSize (line 153) | function totalSize() {
  function breadcrumb (line 162) | function breadcrumb(folderId) {
  function fullPath (line 172) | function fullPath(nodeId) {
  function autoPos (line 187) | function autoPos(pid, idx, area) {
  function node (line 207) | function node(id) { return _nodes[id]; }
  function hasChildNamed (line 209) | function hasChildNamed(pid, name) {
  function wouldCycle (line 214) | function wouldCycle(id, newParentId) {
  function remapPositions (line 226) | function remapPositions(oldGX, oldGY, newGX, newGY) {
  function check (line 242) | function check(repair) {
  function fileIds (line 758) | function fileIds() {
  function purgeDeadBranches (line 765) | function purgeDeadBranches(liveFileIds) {
  function flattenDeepContent (line 796) | function flattenDeepContent(MAX_DEPTH = 50) {
  function repairMetadata (line 888) | function repairMetadata() {
  function autoPosBatch (line 908) | function autoPosBatch(pid, items, area) {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (976K chars).
[
  {
    "path": "CONTRIBUTING.md",
    "chars": 10552,
    "preview": "> Thank you for considering a contribution to **SafeNova**. This document explains how to do it properly so your effort "
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2023-2026 DosX\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 97709,
    "preview": "<img src=\"./pics/intro.png\" style=\"display: block; margin: 0 auto; max-width:80%; max-height:80%; border-radius:8px; mar"
  },
  {
    "path": "SECURITY_AUDIT.md",
    "chars": 45580,
    "preview": "# 🛡️ Security Audit Changelog\n\n> This document summarizes the most significant security-related changes introduced durin"
  },
  {
    "path": "src/.server.ps1",
    "chars": 9239,
    "preview": "# Local Dev Server\n# Run: right-click -> \"Run with PowerShell\"  |  or: .\\._server.ps1\n# Requirements: Windows only, no e"
  },
  {
    "path": "src/css/app.css",
    "chars": 86440,
    "preview": "/* ============================================================\n   SafeNova — VS Code Dark Theme\n   ===================="
  },
  {
    "path": "src/index.html",
    "chars": 76349,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-widt"
  },
  {
    "path": "src/js/constants.js",
    "chars": 24657,
    "preview": "'use strict';\n\n/* ============================================================\n   CONSTANTS\n   ========================="
  },
  {
    "path": "src/js/crypto.js",
    "chars": 3184,
    "preview": "'use strict';\n\n/* ============================================================\n   CRYPTO  —  AES-256-GCM + Argon2id (WAS"
  },
  {
    "path": "src/js/db.js",
    "chars": 19428,
    "preview": "'use strict';\n\n/* ============================================================\n   DATABASE  —  IndexedDB abstraction\n   "
  },
  {
    "path": "src/js/desktop.js",
    "chars": 160545,
    "preview": "'use strict';\n\n/* ============================================================\n   SAVE VFS\n   =========================="
  },
  {
    "path": "src/js/detectors/incognito.js",
    "chars": 9130,
    "preview": "'use strict';\n\n/* ============================================================\n   INCOGNITO DETECTOR  v1.6.2 (adapted)\n\n"
  },
  {
    "path": "src/js/docmode.js",
    "chars": 108,
    "preview": "if (localStorage.getItem('snv-doc-hide') === '1') document.documentElement.classList.add('snv-doc-hidden');\n"
  },
  {
    "path": "src/js/fileops.js",
    "chars": 85649,
    "preview": "'use strict';\n\n/* ============================================================\n   FILENAME SANITIZATION\n   ============"
  },
  {
    "path": "src/js/home.js",
    "chars": 44935,
    "preview": "'use strict';\n\n/* ============================================================\n   HOME VIEW\n   ========================="
  },
  {
    "path": "src/js/initlog.js",
    "chars": 2660,
    "preview": "'use strict';\n\n/* ============================================================\n   INITLOG  —  Initialization stage conso"
  },
  {
    "path": "src/js/main.js",
    "chars": 27237,
    "preview": "'use strict';\n\n/* ============================================================\n   CONSOLE SECURITY WARNING\n   =========="
  },
  {
    "path": "src/js/proactive/daemon.js",
    "chars": 153517,
    "preview": "'use strict';\n\n/* ============================================================\n   SafeNova Proactive — Anti-Tamper Runti"
  },
  {
    "path": "src/js/state.js",
    "chars": 36571,
    "preview": "'use strict';\n\n/* ============================================================\n   SESSION ENCRYPTION  —  AES-256-GCM enc"
  },
  {
    "path": "src/js/vfs.js",
    "chars": 43250,
    "preview": "'use strict';\n\n/* ============================================================\n   VFS  —  Virtual File System (in-memory"
  }
]

About this extraction

This page contains the full source code of the DosX-dev/rep3 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (915.8 KB), approximately 221.5k tokens, and a symbol index with 344 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!