This commit is contained in:
thePR0M3TH3AN committed 2026-02-23 17:26:14 -05:00
1 parent b298eb017c
commit 966701fb1a
32 files changed
+820 -10027

No files matched your search

-34
View File
@@ -1,34 +0,0 @@
const { buildNav } = require('../src/generator');
test('generates navigation tree', () => {
const pages = [
{ file: 'guide/install.md', data: { title: 'Install', order: 1 } },
{ file: 'guide/usage.md', data: { title: 'Usage', order: 2 } },
{ file: 'guide/nested/info.md', data: { title: 'Info', order: 1 } }
];
const tree = buildNav(pages);
const guide = tree.find(n => n.name === 'guide');
expect(guide).toBeDefined();
expect(guide.children.length).toBe(3);
const install = guide.children.find(c => c.name === 'install.md');
expect(install.path).toBe('/guide/install.html');
});
test('adds display names and section flags', () => {
const pages = [
{ file: '02-api.md', data: { title: 'API', order: 2 } },
{ file: '01-guide/index.md', data: { title: 'Guide', order: 1 } },
{ file: '01-guide/setup.md', data: { title: 'Setup', order: 2 } },
{ file: 'index.md', data: { title: 'Home', order: 10 } }
];
const nav = buildNav(pages);
expect(nav[0].name).toBe('index.md');
const guide = nav.find(n => n.name === '01-guide');
expect(guide.displayName).toBe('Guide');
expect(guide.isSection).toBe(true);
const api = nav.find(n => n.name === '02-api.md');
expect(api.displayName).toBe('API');
// alphabetical within same order
expect(nav[1].name).toBe('01-guide');
expect(nav[2].name).toBe('02-api.md');
});
-13
View File
@@ -1,13 +0,0 @@
const fs = require('fs');
const path = require('path');
const loadConfig = require('../src/config/loadConfig');
test('loads configuration and merges defaults', () => {
const dir = fs.mkdtempSync(path.join(__dirname, 'cfg-'));
const file = path.join(dir, 'config.yaml');
fs.writeFileSync(file, 'site:\n title: Test Site\n');
const cfg = loadConfig(file);
expect(cfg.site.title).toBe('Test Site');
expect(cfg.navigation.search).toBe(true);
fs.rmSync(dir, { recursive: true, force: true });
});
-23
View File
@@ -1,23 +0,0 @@
const fs = require('fs');
const path = require('path');
const loadPlugins = require('../src/config/loadPlugins');
test('plugin hook modifies data', async () => {
const dir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'plugins-'));
const pluginFile = path.join(dir, 'test.plugin.js');
fs.writeFileSync(
pluginFile,
"module.exports = { onParseMarkdown: ({ content }) => ({ content: content + '!!' }) };\n"
);
const plugins = loadPlugins({ pluginsDir: dir, plugins: ['test.plugin'] });
let data = { content: 'hello' };
for (const plugin of plugins) {
if (typeof plugin.onParseMarkdown === 'function') {
const res = await plugin.onParseMarkdown(data);
if (res !== undefined) data = res;
}
}
expect(data.content).toBe('hello!!');
fs.rmSync(dir, { recursive: true, force: true });
});
-77
View File
@@ -1,77 +0,0 @@
jest.mock('@11ty/eleventy', () => {
const fs = require('fs');
const path = require('path');
return class Eleventy {
constructor(input, output) {
this.input = input;
this.output = output;
}
setConfig() {}
async write() {
const walk = d => {
const entries = fs.readdirSync(d, { withFileTypes: true });
let files = [];
for (const e of entries) {
const p = path.join(d, e.name);
if (e.isDirectory()) files = files.concat(walk(p));
else if (p.endsWith('.md')) files.push(p);
}
return files;
};
for (const file of walk(this.input)) {
const rel = path.relative(this.input, file).replace(/\.md$/, '.html');
const dest = path.join(this.output, rel);
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, '<header></header><aside class="sidebar"></aside>');
}
}
};
});
const fs = require('fs');
const path = require('path');
const os = require('os');
const { generate } = require('../src/generator');
function getPaths(tree) {
const paths = [];
for (const node of tree) {
if (node.path) paths.push(node.path);
if (node.children) paths.push(...getPaths(node.children));
}
return paths;
}
test('markdown files render with layout and appear in nav/search', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'df-test-'));
const contentDir = path.join(tmp, 'content');
const outputDir = path.join(tmp, '_site');
fs.mkdirSync(path.join(contentDir, 'guide'), { recursive: true });
fs.writeFileSync(path.join(contentDir, 'index.md'), '# Home\nWelcome');
fs.writeFileSync(path.join(contentDir, 'guide', 'install.md'), '# Install\nSteps');
const configPath = path.join(tmp, 'config.yaml');
fs.writeFileSync(configPath, 'site:\n title: Test\n');
await generate({ contentDir, outputDir, configPath });
const indexHtml = fs.readFileSync(path.join(outputDir, 'index.html'), 'utf8');
const installHtml = fs.readFileSync(path.join(outputDir, 'guide', 'install.html'), 'utf8');
expect(indexHtml).toContain('<header');
expect(indexHtml).toContain('<aside class="sidebar"');
expect(installHtml).toContain('<header');
expect(installHtml).toContain('<aside class="sidebar"');
const nav = JSON.parse(fs.readFileSync(path.join(outputDir, 'navigation.json'), 'utf8'));
const navPaths = getPaths(nav);
expect(navPaths).toContain('/index.html');
expect(navPaths).toContain('/guide/install.html');
const search = JSON.parse(fs.readFileSync(path.join(outputDir, 'search-index.json'), 'utf8'));
const docs = search.docs.map(d => d.id);
expect(docs).toContain('index.html');
expect(docs).toContain('guide/install.html');
const installDoc = search.docs.find(d => d.id === 'guide/install.html');
expect(installDoc.body).toContain('Steps');
fs.rmSync(tmp, { recursive: true, force: true });
});
-128
View File
@@ -1,128 +0,0 @@
jest.mock('@11ty/eleventy', () => {
const fs = require('fs');
const path = require('path');
return class Eleventy {
constructor(input, output) {
this.input = input;
this.output = output;
}
setConfig() {}
async write() {
const walk = d => {
const entries = fs.readdirSync(d, { withFileTypes: true });
let files = [];
for (const e of entries) {
const p = path.join(d, e.name);
if (e.isDirectory()) files = files.concat(walk(p));
else if (p.endsWith('.md')) files.push(p);
}
return files;
};
for (const file of walk(this.input)) {
const rel = path.relative(this.input, file).replace(/\.md$/, '.html');
const dest = path.join(this.output, rel);
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(
dest,
`<!DOCTYPE html><html><head><link rel="stylesheet" href="/assets/theme.css"></head><body><header><button id="sidebar-toggle" class="sidebar-toggle">☰</button></header><div class="container"><aside class="sidebar"></aside><main></main></div><script src="/assets/theme.js"></script></body></html>`
);
}
}
};
});
const fs = require('fs');
const path = require('path');
const http = require('http');
const os = require('os');
const puppeteer = require('puppeteer');
const { generate } = require('../src/generator');
jest.setTimeout(30000);
let server;
let browser;
let port;
let tmp;
beforeAll(async () => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'df-responsive-'));
const contentDir = path.join(tmp, 'content');
const outputDir = path.join(tmp, '_site');
fs.mkdirSync(contentDir, { recursive: true });
fs.writeFileSync(path.join(contentDir, 'index.md'), '# Home\n');
await generate({ contentDir, outputDir });
fs.cpSync(path.join(__dirname, '../assets'), path.join(outputDir, 'assets'), { recursive: true });
server = http.createServer((req, res) => {
let filePath = path.join(outputDir, req.url === '/' ? 'index.html' : req.url);
if (req.url.startsWith('/assets')) {
filePath = path.join(outputDir, req.url);
}
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
const ext = path.extname(filePath).slice(1);
const type = { html: 'text/html', js: 'text/javascript', css: 'text/css' }[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': type });
res.end(data);
});
});
await new Promise(resolve => {
server.listen(0, () => {
port = server.address().port;
resolve();
});
});
browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
});
afterAll(async () => {
if (browser) await browser.close();
if (server) server.close();
fs.rmSync(tmp, { recursive: true, force: true });
});
test('sidebar opens on small screens', async () => {
const page = await browser.newPage();
await page.setViewport({ width: 500, height: 800 });
await page.goto(`http://localhost:${port}/`);
await page.waitForSelector('#sidebar-toggle');
await page.click('#sidebar-toggle');
await new Promise(r => setTimeout(r, 300));
const bodyClass = await page.evaluate(() => document.body.classList.contains('sidebar-open'));
const sidebarLeft = await page.evaluate(() => getComputedStyle(document.querySelector('.sidebar')).left);
expect(bodyClass).toBe(true);
expect(sidebarLeft).toBe('0px');
});
test('clicking outside closes sidebar on small screens', async () => {
const page = await browser.newPage();
await page.setViewport({ width: 500, height: 800 });
await page.goto(`http://localhost:${port}/`);
await page.waitForSelector('#sidebar-toggle');
await page.click('#sidebar-toggle');
await new Promise(r => setTimeout(r, 300));
await page.click('main');
await new Promise(r => setTimeout(r, 300));
const bodyClass = await page.evaluate(() => document.body.classList.contains('sidebar-open'));
expect(bodyClass).toBe(false);
});
test('sidebar toggles on large screens', async () => {
const page = await browser.newPage();
await page.setViewport({ width: 1024, height: 800 });
await page.goto(`http://localhost:${port}/`);
await page.waitForSelector('#sidebar-toggle');
await new Promise(r => setTimeout(r, 300));
let sidebarWidth = await page.evaluate(() => getComputedStyle(document.querySelector('.sidebar')).width);
expect(sidebarWidth).toBe('240px');
await page.click('#sidebar-toggle');
await new Promise(r => setTimeout(r, 300));
sidebarWidth = await page.evaluate(() => getComputedStyle(document.querySelector('.sidebar')).width);
expect(sidebarWidth).toBe('0px');
});
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
function main() {
const args = process.argv.slice(2);
const install = args.includes('--install');
const targetArg = args.find(a => !a.startsWith('-')) || '.';
const targetDir = path.resolve(process.cwd(), targetArg);
const templateDir = path.join(__dirname, '..', 'starter');
copyDir(templateDir, targetDir);
const pkgPath = path.join(targetDir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const version = require('../package.json').version;
if (pkg.dependencies && pkg.dependencies.archivox)
pkg.dependencies.archivox = `^${version}`;
pkg.name = path.basename(targetDir);
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
}
if (install) {
execSync('npm install', { cwd: targetDir, stdio: 'inherit' });
}
console.log(`Archivox starter created at ${targetDir}`);
}
main();
-15
View File
@@ -1,15 +0,0 @@
#!/usr/bin/env node
const path = require('path');
const { generate } = require('./src/generator');
(async () => {
try {
const contentDir = path.join(__dirname, 'docs', 'content');
const configPath = path.join(__dirname, 'docs', 'config.yaml');
const outputDir = path.join(__dirname, '_site');
await generate({ contentDir, outputDir, configPath });
} catch (err) {
console.error(err);
process.exit(1);
}
})();
-12
View File
@@ -1,12 +0,0 @@
site:
title: "SeedPass Docs"
description: "One seed to rule them all."
navigation:
search: true
footer:
links:
- text: "SeedPass"
url: "https://seedpass.me/"
@@ -1,227 +0,0 @@
# SeedPass Advanced CLI and API Documentation
## Overview
Welcome to the **Advanced CLI and API Documentation** for **SeedPass**, a secure, deterministic password manager built on Bitcoin's BIP‑85 standard. This guide is designed for power users, developers, and system administrators who wish to leverage the full capabilities of SeedPass through the command line for scripting, automation, and integration.
SeedPass uses a `noun-verb` command structure (e.g., `seedpass entry get <query>`) for a clear, scalable, and discoverable interface. You can explore the available actions for any command group with the `--help` flag (for example, `seedpass entry --help`).
The commands in this document reflect the Typer-based CLI shipped with SeedPass. Each command accepts the optional `--fingerprint` flag to operate on a specific seed profile.
---
## Table of Contents
1. [Global Options](#global-options)
2. [Command Group Reference](#command-group-reference)
- [Entry Commands](#entry-commands)
- [Vault Commands](#vault-commands)
- [Nostr Commands](#nostr-commands)
- [Config Commands](#config-commands)
- [Fingerprint Commands](#fingerprint-commands)
- [Utility Commands](#utility-commands)
- [API Commands](#api-commands)
3. [Detailed Command Descriptions](#detailed-command-descriptions)
4. [API Integration](#api-integration)
5. [Usage Guidelines](#usage-guidelines)
---
## Global Options
These options can be used with any command.
| Flag | Description |
| :--- | :--- |
| `--fingerprint <fp>` | Specify which seed profile to use. If omitted, the most recently used profile is selected. |
| `--help`, `-h` | Display help information for a command or subcommand. |
---
## Command Group Reference
### Entry Commands
Manage individual entries within a vault.
| Action | Command | Examples |
| :--- | :--- | :--- |
| List entries | `entry list` | `seedpass entry list --sort label` |
| Search for entries | `entry search` | `seedpass entry search "GitHub"` |
| Retrieve an entry's secret (password or TOTP code) | `entry get` | `seedpass entry get "GitHub"` |
| Add a password entry | `entry add` | `seedpass entry add Example --length 16 --no-special --exclude-ambiguous` |
| Add a TOTP entry | `entry add-totp` | `seedpass entry add-totp Email --secret JBSW...` |
| Add an SSH key entry | `entry add-ssh` | `seedpass entry add-ssh Server --index 0` |
| Add a PGP key entry | `entry add-pgp` | `seedpass entry add-pgp Personal --user-id [email protected]` |
| Add a Nostr key entry | `entry add-nostr` | `seedpass entry add-nostr Chat` |
| Add a seed phrase entry | `entry add-seed` | `seedpass entry add-seed Backup --words 24` |
| Add a key/value entry | `entry add-key-value` | `seedpass entry add-key-value "API Token" --key api --value abc123` |
| Add a managed account entry | `entry add-managed-account` | `seedpass entry add-managed-account Trading` |
| Modify an entry | `entry modify` | `seedpass entry modify 1 --key new --value updated` |
| Archive an entry | `entry archive` | `seedpass entry archive 1` |
| Unarchive an entry | `entry unarchive` | `seedpass entry unarchive 1` |
| Export all TOTP secrets | `entry export-totp` | `seedpass entry export-totp --file totp.json` |
| Show all TOTP codes | `entry totp-codes` | `seedpass entry totp-codes` |
### Vault Commands
Manage the entire vault for a profile.
| Action | Command | Examples |
| :--- | :--- | :--- |
| Export the vault | `vault export` | `seedpass vault export --file backup.json` |
| Import a vault | `vault import` | `seedpass vault import --file backup.json` *(also syncs with Nostr)* |
| Change the master password | `vault change-password` | `seedpass vault change-password` |
| Lock the vault | `vault lock` | `seedpass vault lock` |
| Show profile statistics | `vault stats` | `seedpass vault stats` |
| Reveal or back up the parent seed | `vault reveal-parent-seed` | `seedpass vault reveal-parent-seed --file backup.enc` |
### Nostr Commands
Interact with the Nostr network for backup and synchronization. Offline mode is enabled by default, so disable it with `seedpass config toggle-offline` before using these commands.
| Action | Command | Examples |
| :--- | :--- | :--- |
| Sync with relays | `nostr sync` | `seedpass nostr sync` |
| Get public key | `nostr get-pubkey` | `seedpass nostr get-pubkey` |
### Config Commands
Manage profile‑specific settings.
| Action | Command | Examples |
| :--- | :--- | :--- |
| Get a setting value | `config get` | `seedpass config get kdf_iterations` |
| Set a setting value | `config set` | `seedpass config set backup_interval 3600` |
| Toggle offline mode | `config toggle-offline` | `seedpass config toggle-offline` |
### Fingerprint Commands
Manage seed profiles (fingerprints).
| Action | Command | Examples |
| :--- | :--- | :--- |
| List all profiles | `fingerprint list` | `seedpass fingerprint list` |
| Add a profile | `fingerprint add` | `seedpass fingerprint add` |
| Remove a profile | `fingerprint remove` | `seedpass fingerprint remove <fp>` |
| Switch profile | `fingerprint switch` | `seedpass fingerprint switch <fp>` |
### Utility Commands
Miscellaneous helper commands.
| Action | Command | Examples |
| :--- | :--- | :--- |
| Generate a password | `util generate-password` | `seedpass util generate-password --length 24 --special-mode safe --exclude-ambiguous` |
| Verify script checksum | `util verify-checksum` | `seedpass util verify-checksum` |
| Update script checksum | `util update-checksum` | `seedpass util update-checksum` |
If you see a startup warning about a script checksum mismatch,
run `seedpass util update-checksum` or choose "Generate Script Checksum"
from the Settings menu to update the stored value.
### API Commands
Run or stop the local HTTP API.
| Action | Command | Examples |
| :--- | :--- | :--- |
| Start the API | `api start` | `seedpass api start --host 0.0.0.0 --port 8000` |
| Stop the API | `api stop --token TOKEN` | `seedpass api stop --token <token>` |
---
## Detailed Command Descriptions
### `entry` Commands
- **`seedpass entry list`** – List entries in the vault, optionally sorted or filtered.
- **`seedpass entry search <query>`** – Search across labels, usernames, URLs and notes. Results show the entry type before each label.
- **`seedpass entry get <query>`** – Retrieve the password or TOTP code for one matching entry, depending on the entry's type.
- **`seedpass entry add <label>`** – Create a new password entry. Use `--length` and flags like `--no-special`, `--special-mode safe`, or `--exclude-ambiguous` to override the global policy.
- **`seedpass entry add-totp <label>`** – Create a TOTP entry. Use `--secret` to import an existing secret or `--index` to derive from the seed.
- **`seedpass entry add-ssh <label>`** – Create an SSH key entry derived from the seed.
- **`seedpass entry add-pgp <label>`** – Create a PGP key entry. Provide `--user-id` and `--key-type` as needed.
- **`seedpass entry add-nostr <label>`** – Create a Nostr key entry for decentralised chat.
- **`seedpass entry add-seed <label>`** – Store a derived seed phrase. Use `--words` to set the word count.
- **`seedpass entry add-key-value <label>`** – Store arbitrary data with `--key` and `--value`.
- **`seedpass entry add-managed-account <label>`** – Store a BIP‑85 derived account seed.
- **`seedpass entry modify <id>`** – Update an entry's fields. For key/value entries you can change the label, key and value.
- **`seedpass entry archive <id>`** – Mark an entry as archived so it is hidden from normal lists.
- **`seedpass entry unarchive <id>`** – Restore an archived entry.
- **`seedpass entry export-totp --file <path>`** – Export all stored TOTP secrets to a JSON file.
- **`seedpass entry totp-codes`** – Display all current TOTP codes with remaining time.
Example retrieving a TOTP code:
```bash
$ seedpass entry get "email"
[##########----------] 15s
Code: 123456
```
### Viewing Entry Details
Picking an entry from `entry list` or `entry search` displays its metadata first
so you can review the label, username and notes. Sensitive fields are hidden
until you confirm you want to reveal them. After showing the secret, the details
view offers the same actions as `entry get`—edit the entry, archive it or show
QR codes for supported types.
### `vault` Commands
- **`seedpass vault export`** – Export the entire vault to an encrypted JSON file.
- **`seedpass vault import`** – Import a vault from an encrypted JSON file and automatically sync via Nostr.
- **`seedpass vault change-password`** – Change the master password used for encryption.
- **`seedpass vault lock`** – Clear sensitive data from memory and require reauthentication.
- **`seedpass vault stats`** – Display statistics about the active seed profile.
- **`seedpass vault reveal-parent-seed`** – Print the parent seed or write an encrypted backup with `--file`.
### `nostr` Commands
- **`seedpass nostr sync`** – Perform a two‑way sync with configured Nostr relays.
- **`seedpass nostr get-pubkey`** – Display the Nostr public key for the active profile.
### `config` Commands
- **`seedpass config get <key>`** – Retrieve a configuration value such as `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, `clipboard_clear_delay`, `additional_backup_path`, `relays`, `quick_unlock`, `nostr_max_retries`, `nostr_retry_delay`, or password policy fields like `min_uppercase`.
- **`seedpass config set <key> <value>`** – Update a configuration option. Example: `seedpass config set kdf_iterations 200000`. Use keys like `min_uppercase`, `min_lowercase`, `min_digits`, `min_special`, `include_special_chars`, `allowed_special_chars`, `special_mode`, `exclude_ambiguous`, `nostr_max_retries`, `nostr_retry_delay`, or `quick_unlock` to adjust settings.
- **`seedpass config toggle-secret-mode`** – Interactively enable or disable Secret Mode and set the clipboard delay.
- **`seedpass config toggle-offline`** – Enable or disable offline mode to skip Nostr operations.
### `fingerprint` Commands
- **`seedpass fingerprint list`** – List available profiles by fingerprint.
- **`seedpass fingerprint add`** – Create a new seed profile.
- **`seedpass fingerprint remove <fp>`** – Delete the specified profile.
- **`seedpass fingerprint switch <fp>`** – Switch the active profile.
### `util` Commands
- **`seedpass util generate-password`** – Generate a strong password of the requested length.
- **`seedpass util verify-checksum`** – Verify the SeedPass script checksum.
- **`seedpass util update-checksum`** – Regenerate the script checksum file.
---
## API Integration
SeedPass provides a small REST API for automation. Run `seedpass api start` to launch the server. The command prints a one‑time token which clients must include in the `Authorization` header.
Set the `SEEDPASS_CORS_ORIGINS` environment variable to a comma‑separated list of allowed origins when you need cross‑origin requests:
```bash
SEEDPASS_CORS_ORIGINS=http://localhost:3000 seedpass api start
```
Shut down the server with `seedpass api stop --token <token>`.
---
## Usage Guidelines
- Use the `--help` flag for details on any command.
- Set a strong master password and regularly export encrypted backups.
- Adjust configuration values like `kdf_iterations`, `backup_interval`, `inactivity_timeout`, `secret_mode_enabled`, `nostr_max_retries`, `nostr_retry_delay`, or `quick_unlock` through the `config` commands.
- Customize the global password policy with commands like `config set min_uppercase 3` or `config set special_mode safe`. When adding a password interactively you can override these values, choose a safe special-character set, and exclude ambiguous characters.
- `entry get` is script‑friendly and can be piped into other commands.
@@ -1,299 +0,0 @@
# SeedPass REST API Reference
This guide covers how to start the SeedPass API, authenticate requests, and interact with the available endpoints.
**Note:** All UI layers, including the CLI, BeeWare GUI, and future adapters, consume this REST API through service classes in `seedpass.core`. See [docs/gui_adapter.md](docs/gui_adapter.md) for more details on the GUI integration.
## Starting the API
Run `seedpass api start` from your terminal. The command prints a short‑lived JWT token used for authentication:
```bash
$ seedpass api start
API token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
```
Keep this token secret and avoid logging it. Tokens expire after a few minutes and every request must include one in the `Authorization` header using the `Bearer` scheme.
## Endpoints
- `GET /api/v1/entry?query=<text>` – Search entries matching a query.
- `GET /api/v1/entry/{id}` – Retrieve a single entry by its index. Requires an `X-SeedPass-Password` header.
- `POST /api/v1/entry` – Create a new entry of any supported type.
- `PUT /api/v1/entry/{id}` – Modify an existing entry.
- `PUT /api/v1/config/{key}` – Update a configuration value.
- `POST /api/v1/secret-mode` – Enable or disable Secret Mode and set the clipboard delay.
- `POST /api/v1/entry/{id}/archive` – Archive an entry.
- `POST /api/v1/entry/{id}/unarchive` – Unarchive an entry.
- `GET /api/v1/config/{key}` – Return the value for a configuration key.
- `GET /api/v1/fingerprint` – List available seed fingerprints.
- `POST /api/v1/fingerprint` – Add a new seed fingerprint.
- `DELETE /api/v1/fingerprint/{fp}` – Remove a fingerprint.
- `POST /api/v1/fingerprint/select` – Switch the active fingerprint.
- `GET /api/v1/totp/export` – Export all TOTP entries as JSON. Requires an `X-SeedPass-Password` header.
- `GET /api/v1/totp` – Return current TOTP codes and remaining time. Requires an `X-SeedPass-Password` header.
- `GET /api/v1/stats` – Return statistics about the active seed profile.
- `GET /api/v1/notifications` – Retrieve and clear queued notifications. Messages appear in the persistent notification box but remain queued until fetched.
- `GET /api/v1/nostr/pubkey` – Fetch the Nostr public key for the active seed.
- `POST /api/v1/checksum/verify` – Verify the checksum of the running script.
- `POST /api/v1/checksum/update` – Update the stored script checksum.
- `POST /api/v1/change-password` – Change the master password for the active profile.
- `POST /api/v1/vault/import` – Import a vault backup from a file or path.
- `POST /api/v1/vault/export` – Export the vault and download the encrypted file. Requires an additional `X-SeedPass-Password` header.
- `POST /api/v1/vault/backup-parent-seed` – Save an encrypted backup of the parent seed. Requires a `confirm` flag in the request body and an `X-SeedPass-Password` header.
- `POST /api/v1/vault/lock` – Lock the vault and clear sensitive data from memory.
- `GET /api/v1/relays` – List configured Nostr relays.
- `POST /api/v1/relays` – Add a relay URL.
- `DELETE /api/v1/relays/{idx}` – Remove the relay at the given index (1‑based).
- `POST /api/v1/relays/reset` – Reset the relay list to defaults.
- `POST /api/v1/shutdown` – Stop the server gracefully.
## Secure Deployment
Always run the API behind HTTPS. Use a reverse proxy such as Nginx or Caddy to terminate TLS and forward requests to SeedPass. Example Nginx configuration:
```
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
For local testing, Uvicorn can serve TLS directly:
```
uvicorn seedpass.api:app --ssl-certfile=cert.pem --ssl-keyfile=key.pem
```
## Example Requests
Send requests with the token in the header:
```bash
curl -H "Authorization: Bearer <token>" \
"https://127.0.0.1:8000/api/v1/entry?query=email"
```
### Creating an Entry
`POST /api/v1/entry` accepts a JSON body with at least a `label` field. Set
`type` (or `kind`) to choose the entry variant (`password`, `totp`, `ssh`, `pgp`,
`nostr`, `seed`, `key_value`, or `managed_account`). Additional fields vary by
type:
- **password** – `length`, optional `username`, `url` and `notes`
- **totp** – `secret` or `index`, optional `period`, `digits`, `notes`, `archived`
- **ssh/nostr/seed/managed_account** – `index`, optional `notes`, `archived`
- **pgp** – `index`, `key_type`, `user_id`, optional `notes`, `archived`
- **key_value** – `value`, optional `notes`
Example creating a TOTP entry:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/entry \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"type": "totp", "label": "Email", "secret": "JBSW..."}'
```
### Updating an Entry
Use `PUT /api/v1/entry/{id}` to change fields such as `label`, `username`,
`url`, `notes`, `period`, `digits` or `value` depending on the entry type.
```bash
curl -X PUT http://127.0.0.1:8000/api/v1/entry/1 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"username": "alice"}'
```
### Updating Configuration
Send a JSON body containing a `value` field to `PUT /api/v1/config/{key}`:
```bash
curl -X PUT http://127.0.0.1:8000/api/v1/config/inactivity_timeout \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"value": 300}'
```
To raise the PBKDF2 work factor or change how often backups are written:
```bash
curl -X PUT http://127.0.0.1:8000/api/v1/config/kdf_iterations \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"value": 200000}'
curl -X PUT http://127.0.0.1:8000/api/v1/config/backup_interval \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"value": 3600}'
```
Using fewer iterations or a long interval reduces security, so adjust these values carefully.
### Toggling Secret Mode
Send both `enabled` and `delay` values to `/api/v1/secret-mode`:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/secret-mode \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "delay": 20}'
```
### Switching Fingerprints
Change the active seed profile via `POST /api/v1/fingerprint/select`:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/fingerprint/select \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"fingerprint": "abc123"}'
```
### Exporting the Vault
Download an encrypted vault backup via `POST /api/v1/vault/export`:
```bash
curl -X POST https://127.0.0.1:8000/api/v1/vault/export \
-H "Authorization: Bearer <token>" \
-H "X-SeedPass-Password: <master-password>" \
-o backup.json
```
### Importing a Vault
Restore a backup with `POST /api/v1/vault/import`. Use `-F` to upload a file:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/vault/import \
-H "Authorization: Bearer <token>" \
-F file=@backup.json
```
### Locking the Vault
Clear sensitive data from memory using `/api/v1/vault/lock`:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/vault/lock \
-H "Authorization: Bearer <token>"
```
### Backing Up the Parent Seed
Trigger an encrypted seed backup with `/api/v1/vault/backup-parent-seed`:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/vault/backup-parent-seed \
-H "Authorization: Bearer <token>" \
-H "X-SeedPass-Password: <master password>" \
-H "Content-Type: application/json" \
-d '{"path": "seed_backup.enc", "confirm": true}'
```
### Retrieving Vault Statistics
Get profile stats such as entry counts with `GET /api/v1/stats`:
```bash
curl -H "Authorization: Bearer <token>" \
http://127.0.0.1:8000/api/v1/stats
```
### Checking Notifications
Get queued messages with `GET /api/v1/notifications`:
```bash
curl -H "Authorization: Bearer <token>" \
http://127.0.0.1:8000/api/v1/notifications
```
The TUI displays these alerts in a persistent notification box for 10 seconds,
but the endpoint returns all queued messages even if they have already
disappeared from the screen.
### Changing the Master Password
Update the vault password via `POST /api/v1/change-password`:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/change-password \
-H "Authorization: Bearer <token>"
```
### Verifying the Script Checksum
Check that the running script matches the stored checksum:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/checksum/verify \
-H "Authorization: Bearer <token>"
```
### Updating the Script Checksum
Regenerate the stored checksum using `/api/v1/checksum/update`:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/checksum/update \
-H "Authorization: Bearer <token>"
```
### Managing Relays
List, add, or remove Nostr relays:
```bash
# list
curl -H "Authorization: Bearer <token>" http://127.0.0.1:8000/api/v1/relays
# add
curl -X POST http://127.0.0.1:8000/api/v1/relays \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"url": "wss://relay.example.com"}'
# remove first relay
curl -X DELETE http://127.0.0.1:8000/api/v1/relays/1 \
-H "Authorization: Bearer <token>"
# reset to defaults
curl -X POST http://127.0.0.1:8000/api/v1/relays/reset \
-H "Authorization: Bearer <token>"
```
### Enabling CORS
Cross‑origin requests are disabled by default. Set `SEEDPASS_CORS_ORIGINS` to a comma‑separated list of allowed origins before starting the API:
```bash
SEEDPASS_CORS_ORIGINS=http://localhost:3000 seedpass api start
```
Browsers can then call the API from the specified origins, for example using JavaScript:
```javascript
fetch('http://127.0.0.1:8000/api/v1/entry?query=email', {
headers: { Authorization: 'Bearer <token>' }
});
```
Without CORS enabled, only same‑origin or command‑line tools like `curl` can access the API.
@@ -1,596 +0,0 @@
# SeedPass JSON Entry Management and Extensibility
## Table of Contents
- [Introduction](#introduction)
- [JSON Schema for Individual Entries](#json-schema-for-individual-entries)
- [General Structure](#general-structure)
- [Field Descriptions](#field-descriptions)
- [Example Entries](#example-entries)
- [Handling `kind` Types and Extensibility](#handling-kind-types-and-extensibility)
- [Extensible JSON Schema Design](#extensible-json-schema-design)
- [Ensuring Backward Compatibility](#ensuring-backward-compatibility)
- [Best Practices for Adding New Kinds](#best-practices-for-adding-new-kinds)
- [Adding New `kind` Types](#adding-new-kind-types)
- [Example: Adding `cryptocurrency_wallet`](#example-adding-cryptocurrency_wallet)
- [Backup and Rollback Mechanism](#backup-and-rollback-mechanism)
- [Security Considerations](#security-considerations)
- [Updated Roadmap](#updated-roadmap)
- [Phase 1: Core Functionality and Security Enhancements](#phase-1-core-functionality-and-security-enhancements)
- [Phase 2: Enhanced Security and Data Management](#phase-2-enhanced-security-and-data-management)
- [Phase 3: Advanced CLI Functionalities](#phase-3-advanced-cli-functionalities)
- [Phase 4: Data Management Enhancements and Integrations](#phase-4-data-management-enhancements-and-integrations)
- [Phase 5: Documentation, Testing, and Finalization](#phase-5-documentation-testing-and-finalization)
- [Future Phases (Beyond Initial Roadmap)](#future-phases-beyond-initial-roadmap)
- [Summary](#summary)
- [Contact](#contact)
---
## Introduction
**SeedPass** is a secure password generator and manager leveraging **Bitcoin's BIP-85 standard** and integrating with the **Nostr network** for decentralized synchronization. Instead of pushing one large index file, SeedPass posts **snapshot chunks** of the index followed by lightweight **delta events** whenever changes occur. This chunked approach improves reliability and keeps bandwidth usage minimal. To enhance modularity, scalability, and security, SeedPass stores all entries in a single encrypted index file named `seedpass_entries_db.json.enc`. This document outlines the entry management system, ensuring that new `kind` types can be added seamlessly without disrupting existing functionalities.
---
## Index File Format
All entries belonging to a seed profile are stored in an encrypted file named `seedpass_entries_db.json.enc`. This index uses `schema_version` `3` and contains an `entries` object keyed by numeric identifiers.
```json
{
"schema_version": 3,
"entries": {
"0": {
"label": "example.com",
"length": 8,
"username": "user",
"url": "https://example.com",
"archived": false,
"type": "password",
"kind": "password",
"notes": "",
"custom_fields": [],
"origin": ""
}
}
}
```
---
## JSON Schema for Individual Entries
Each entry is stored within `seedpass_entries_db.json.enc` under the `entries` dictionary. The structure supports diverse entry types (`kind`) and allows for future expansions.
### General Structure
```json
{
"label": "Example",
"length": 8,
"username": "[email protected]",
"url": "https://example.com",
"archived": false,
"type": "password",
"kind": "password",
"notes": "",
"custom_fields": [],
"origin": "",
"tags": [],
"index": 0
}
```
### Field Descriptions
- **label** (`string`): Descriptive name for the entry (e.g., website or service).
- **length** (`integer`, optional): Desired password length for generated passwords.
- **username** (`string`, optional): Username associated with the entry.
- **url** (`string`, optional): Website or service URL.
- **archived** (`boolean`): Marks the entry as archived when `true`.
- **type** (`string`): The entry type (`password`, `totp`, `ssh`, `seed`, `pgp`, `nostr`, `note`, `key_value`).
- **kind** (`string`): Synonym for `type` kept for backward compatibility.
- **notes** (`string`): Free-form notes.
- **custom_fields** (`array`, optional): Additional user-defined fields.
- **origin** (`string`, optional): Source identifier for imported data.
- **value** (`string`, optional): For `key_value` entries, stores the secret value.
- **key** (`string`, optional): Name of the key for `key_value` entries.
- **index** (`integer`, optional): BIP-85 derivation index for entries that derive material from a seed.
- **word_count** (`integer`, managed_account only): Number of words in the child seed. Managed accounts always use `12`.
- **fingerprint** (`string`, managed_account only): Identifier of the child profile, used for its directory name.
- **tags** (`array`, optional): Category labels to aid in organization and search.
#### Password Policy Fields
- **min_uppercase** (`integer`, default `2`): Minimum required uppercase letters.
- **min_lowercase** (`integer`, default `2`): Minimum required lowercase letters.
- **min_digits** (`integer`, default `2`): Minimum required digits.
- **min_special** (`integer`, default `2`): Minimum required special characters.
- **include_special_chars** (`boolean`, default `true`): Enable or disable any punctuation in generated passwords.
- **allowed_special_chars** (`string`, optional): Restrict punctuation to this exact set.
- **special_mode** (`string`, default `"standard"`): Choose `"safe"` for the `SAFE_SPECIAL_CHARS` set (`!@#$%^*-_+=?`), otherwise the full `string.punctuation` is used.
- **exclude_ambiguous** (`boolean`, default `false`): Omit confusing characters like `O0Il1`.
Example:
```json
"custom_fields": [
{"name": "account_id", "value": "123"},
{"name": "recovery_hint", "value": "mother's maiden name"}
]
```
### Example Entries
#### 1. Generated Password
```json
{
"entry_num": 0,
"index_num": 0,
"fingerprint": "a1b2c3d4",
"kind": "generated_password",
"data": {
"title": "Example Website",
"username": "[email protected]",
"email": "[email protected]",
"url": "https://example.com",
"password": "<encrypted_password>"
},
"custom_fields": [
{"name": "department", "value": "finance"}
],
"tags": ["work"],
"timestamp": "2024-04-27T12:34:56Z",
"metadata": {
"created_at": "2024-04-27T12:34:56Z",
"updated_at": "2024-04-27T12:34:56Z",
"checksum": "abc123def456"
}
}
```
#### 2. Stored Password
```json
{
"entry_num": 1,
"index_num": "q1wec4d426fs",
"fingerprint": "a1b2c3d4",
"kind": "stored_password",
"data": {
"title": "Another Service",
"username": "another_user",
"password": "<encrypted_password>"
},
"timestamp": "2024-04-27T12:35:56Z",
"metadata": {
"created_at": "2024-04-27T12:35:56Z",
"updated_at": "2024-04-27T12:35:56Z",
"checksum": "def789ghi012"
}
}
```
#### Password Entry with Policy Overrides
```json
{
"label": "Custom Policy",
"length": 16,
"include_special_chars": false,
"exclude_ambiguous": true
}
```
#### 3. Managed User
```json
{
"entry_num": 2,
"index_num": "a1b2c3d4e5f6",
"fingerprint": "a1b2c3d4",
"kind": "managed_user",
"data": {
"users_password": "<encrypted_users_password>"
},
"timestamp": "2024-04-27T12:36:56Z",
"metadata": {
"created_at": "2024-04-27T12:36:56Z",
"updated_at": "2024-04-27T12:36:56Z",
"checksum": "ghi345jkl678"
}
}
```
#### 4. 12 Word Seed
```json
{
"entry_num": 3,
"index_num": "f7g8h9i0j1k2",
"fingerprint": "a1b2c3d4",
"kind": "12_word_seed",
"data": {
"seed_phrase": "<encrypted_seed_phrase>"
},
"timestamp": "2024-04-27T12:37:56Z",
"metadata": {
"created_at": "2024-04-27T12:37:56Z",
"updated_at": "2024-04-27T12:37:56Z",
"checksum": "jkl901mno234"
}
}
```
#### 5. Nostr Keys
```json
{
"entry_num": 4,
"index_num": "l3m4n5o6p7q8",
"fingerprint": "a1b2c3d4",
"kind": "nostr_keys",
"data": {
"public_key": "<public_key>",
"private_key": "<encrypted_private_key>"
},
"timestamp": "2024-04-27T12:38:56Z",
"metadata": {
"created_at": "2024-04-27T12:38:56Z",
"updated_at": "2024-04-27T12:38:56Z",
"checksum": "mno567pqr890"
}
}
```
#### 6. Note
```json
{
"entry_num": 5,
"index_num": "r9s0t1u2v3w4",
"fingerprint": "a1b2c3d4",
"kind": "note",
"data": {
"content": "This is a secure note.",
"tags": ["personal", "secure"]
},
"timestamp": "2024-04-27T12:39:56Z",
"metadata": {
"created_at": "2024-04-27T12:39:56Z",
"updated_at": "2024-04-27T12:39:56Z",
"checksum": "pqr345stu678"
}
}
```
#### 7. Key/Value
```json
{
"entry_num": 6,
"fingerprint": "a1b2c3d4",
"kind": "key_value",
"data": {
"key": "api_key",
"value": "<encrypted_value>"
},
"tags": ["api"],
"timestamp": "2024-04-27T12:40:56Z"
}
```
#### 8. Managed Account
```json
{
"entry_num": 7,
"fingerprint": "a1b2c3d4",
"kind": "managed_account",
"data": {
"account": "[email protected]",
"password": "<encrypted_password>"
},
"timestamp": "2024-04-27T12:41:56Z"
}
```
Managed accounts store a child seed derived from the parent profile. The entry is saved under
`.seedpass/<parent_fp>/accounts/<child_fp>` where `<child_fp>` is the managed account's
fingerprint. When loaded, the CLI displays a breadcrumb like `<parent_fp> > Managed Account > <child_fp>`.
Press **Enter** on the main menu to exit back to the parent profile.
The `key` field is purely descriptive, while `value` holds the sensitive string
such as an API token. Notes and custom fields may also be included alongside the
standard metadata.
---
## Handling `kind` Types and Extensibility
### Extensible JSON Schema Design
The JSON schema is designed to be **extensible** and **forward-compatible**, allowing new `kind` types to be added without impacting existing functionalities.
#### a. Core Structure
Each entry is encapsulated in its own JSON file with a standardized structure:
```json
{
"entry_num": 0,
"index_num": 0,
"fingerprint": "a1b2c3d4",
"kind": "generated_password",
"data": {
// Fields specific to the kind
},
"timestamp": "2024-04-27T12:34:56Z",
"metadata": {
"created_at": "2024-04-27T12:34:56Z",
"updated_at": "2024-04-27T12:34:56Z",
"checksum": "<checksum_value>"
}
}
```
#### b. The `kind` Field
- **Purpose:** Specifies the type of entry.
- **Flexibility:** As a simple string identifier, new `kind` values can be introduced without altering the existing schema.
**Example:**
```json
"kind": "cryptocurrency_wallet"
```
#### c. The `data` Object
- **Purpose:** Contains fields specific to the `kind`.
- **Extensibility:** Each `kind` can define its unique set of fields without affecting others.
**Example for a New Kind (`cryptocurrency_wallet`):**
```json
"data": {
"wallet_name": "My Bitcoin Wallet",
"address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"private_key": "<encrypted_private_key>"
}
```
### Ensuring Backward Compatibility
To maintain compatibility as new `kind` types are introduced, implement the following practices:
#### a. Graceful Handling of Unknown Kinds
- **Implementation:** When encountering an unrecognized `kind`, handle it gracefully by ignoring the entry, logging a warning, or providing a default handling mechanism.
- **Benefit:** Prevents the application from crashing or misbehaving due to unrecognized `kind` types.
**Pseudo-Code Example:**
```python
def process_entry(entry):
kind = entry.get("kind")
data = entry.get("data")
fingerprint = entry.get("fingerprint")
if kind == "generated_password":
handle_generated_password(data, fingerprint)
elif kind == "stored_password":
handle_stored_password(data, fingerprint)
# ... other known kinds ...
else:
log_warning(f"Unknown kind: {kind}. Skipping entry.")
```
#### b. Versioning the Schema
- **Implementation:** Introduce a `schema_version` or `seedpass_version` field to indicate the version of the JSON schema being used.
- **Benefit:** Facilitates future updates and migrations by clearly identifying the schema version.
**Example:**
```json
"seedpass_version": "1.0.0"
```
#### c. Documentation and Standards
- **Maintain Clear Documentation:** Keep comprehensive documentation for each `kind`, detailing required and optional fields.
- **Adhere to Standards:** Follow consistent naming conventions and data types to ensure uniformity across different `kind` types.
### Best Practices for Adding New Kinds
To ensure seamless integration of new `kind` types in the future, consider the following best practices:
#### a. Consistent Naming Conventions
- **Use Clear and Descriptive Names:** Aids in readability and maintenance.
- **Avoid Reserved Keywords:** Ensure `kind` names do not clash with existing or future reserved keywords within the application or JSON standards.
#### b. Modular Code Architecture
- **Separate Handlers:** Implement separate functions or modules for handling each `kind`. Promotes code modularity and easier maintenance.
**Example:**
```python
# handlers.py
def handle_generated_password(data, fingerprint):
# Implementation
def handle_stored_password(data, fingerprint):
# Implementation
def handle_cryptocurrency_wallet(data, fingerprint):
# Implementation
```
#### c. Validation and Error Handling
- **Validate Data Fields:** Ensure each `kind` has the necessary fields before processing.
- **Handle Missing or Extra Fields:** Implement logic to manage incomplete or unexpected data gracefully.
**Example:**
```python
def handle_cryptocurrency_wallet(data, fingerprint):
required_fields = ["wallet_name", "address", "private_key"]
for field in required_fields:
if field not in data:
raise ValueError(f"Missing required field '{field}' in cryptocurrency_wallet entry.")
# Proceed with processing
```
#### d. Backward Compatibility Testing
- **Automated Tests:** Develop tests that verify the application's ability to handle both existing and new `kind` types.
- **Regression Testing:** Ensure adding new kinds does not inadvertently affect existing functionalities.
---
## Adding New `kind` Types
Adding new `kind` types is straightforward due to the extensible JSON schema design. Below is a step-by-step guide to adding a new `kind` without affecting existing functionalities.
### Example: Adding `cryptocurrency_wallet`
#### a. Define the New Kind Structure
Create a JSON file following the standardized structure with the new `kind` value.
```json
{
"entry_num": 6,
"index_num": "x1y2z3a4b5c6",
"fingerprint": "a1b2c3d4",
"kind": "cryptocurrency_wallet",
"data": {
"wallet_name": "My Bitcoin Wallet",
"address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
"private_key": "<encrypted_private_key>"
},
"timestamp": "2024-04-27T12:40:56Z",
"metadata": {
"created_at": "2024-04-27T12:40:56Z",
"updated_at": "2024-04-27T12:40:56Z",
"checksum": "stu901vwx234"
}
}
```
#### b. Update the Application to Handle the New Kind
**Implement Handler Function:**
```python
def handle_cryptocurrency_wallet(data, fingerprint):
wallet_name = data.get("wallet_name")
address = data.get("address")
private_key = decrypt(data.get("private_key"))
# Process the cryptocurrency wallet entry
# e.g., store in memory, display to user, etc.
```
**Integrate the Handler:**
```python
def process_entry(entry):
kind = entry.get("kind")
data = entry.get("data")
fingerprint = entry.get("fingerprint")
if kind == "generated_password":
handle_generated_password(data, fingerprint)
elif kind == "stored_password":
handle_stored_password(data, fingerprint)
elif kind == "cryptocurrency_wallet":
handle_cryptocurrency_wallet(data, fingerprint)
# ... other known kinds ...
else:
log_warning(f"Unknown kind: {kind}. Skipping entry.")
```
#### c. No Impact on Existing Kinds
Existing kinds such as `generated_password`, `stored_password`, etc., continue to operate without any changes. The introduction of `cryptocurrency_wallet` is additive and does not interfere with the processing of other kinds.
---
## Backup and Rollback Mechanism
To ensure data integrity and provide recovery options, SeedPass implements a robust backup and rollback system within the **Fingerprint-Based Backup and Local Storage** framework.
### Backup Directory Structure
All backups are organized based on fingerprints, ensuring that each seed's data remains isolated and secure.
```
~/.seedpass/
├── a1b2c3d4/
│ ├── entries/
│ │ ├── entry_0.json
│ │ ├── entry_1.json
│ │ └── ...
│ ├── backups/
│ │ ├── entry_0_v1.json
│ │ ├── entry_0_v2.json
│ │ ├── entry_1_v1.json
│ │ └── ...
│ ├── parent_seed.enc
│ ├── seedpass_entries_db_checksum.txt
│ └── seedpass_entries_db.json
├── b5c6d7e8/
│ ├── entries/
│ │ ├── entry_0.json
│ │ ├── entry_1.json
│ │ └── ...
│ ├── backups/
│ │ ├── entry_0_v1.json
│ │ ├── entry_0_v2.json
│ │ ├── entry_1_v1.json
│ │ └── ...
│ ├── parent_seed.enc
│ ├── seedpass_entries_db_checksum.txt
│ └── seedpass_entries_db.json
└── ...
```
### Backup Process
1. **Upon Modifying an Entry:**
- The current version of the entry is copied to the `backups/` directory within the corresponding fingerprint folder with a version suffix (e.g., `entry_0_v1.json`).
- The modified entry is saved in the `entries/` directory within the same fingerprint folder.
2. **Versioning:**
- Each backup file includes a version number to track changes over time.
### Rollback Functionality
- **Restoring an Entry:**
- Users can select a backup version from the `backups/` directory within the specific fingerprint folder.
- The selected backup file is copied back to the `entries/` directory, replacing the current version.
**Example Command:**
```bash
seedpass rollback --fingerprint a1b2c3d4 --file entry_0_v1.json
```
**Example Directory Structure After Rollback:**
```
~/.seedpass/
├── a1b2c3d4/
│ ├── entries/
│ │ ├── entry_0.json # Restored from entry_0_v1.json
│ │ ├── entry_1.json
│ │ └── ...
│ ├── backups/
│ │ ├── entry_0_v1.json
│ │ ├── entry_0_v2.json
│ │ ├── entry_1_v1.json
│ │ └── ...
│ ├── parent_seed.enc
│ ├── seedpass_script_checksum.txt
│ ├── seedpass_entries_db_checksum.txt
│ └── seedpass_entries_db.json
├── ...
```
*Note: Restoring a backup overwrites the current entry. Ensure that you intend to revert to the selected backup before proceeding.*
@@ -1,49 +0,0 @@
# Index Migrations
SeedPass stores its password index in an encrypted JSON file. Each index contains
a `schema_version` field so the application knows how to upgrade older files.
> **Note:** Recent releases derive passwords and other artifacts using a new deterministic algorithm that works consistently across Python versions. Artifacts produced with older versions will not match outputs from this release and must be regenerated.
## How migrations work
When the vault loads the index, `Vault.load_index()` checks the version and
applies migrations defined in `password_manager/migrations.py`. The
`apply_migrations()` function iterates through registered migrations until the
file reaches `LATEST_VERSION`.
If an old file lacks `schema_version`, it is treated as version 0 and upgraded
to the latest format. Attempting to load an index from a future version will
raise an error.
## Upgrading an index
1. The JSON is decrypted and parsed.
2. `apply_migrations()` applies any necessary steps, such as injecting the
`schema_version` field on first upgrade.
3. After migration, the updated index is saved back to disk.
This process happens automatically; users only need to open their vault to
upgrade older indices.
### Legacy Fernet migration
Older versions stored the vault index in a file named
`seedpass_passwords_db.json.enc` encrypted with Fernet. When opening such a
vault, SeedPass now automatically decrypts the legacy file, re‑encrypts it using
AES‑GCM, and saves it under the new name `seedpass_entries_db.json.enc`.
The original Fernet file is preserved as
`seedpass_entries_db.json.enc.fernet` and the legacy checksum file, if present,
is renamed to `seedpass_entries_db_checksum.txt.fernet`.
No additional command is required – simply open your existing vault and the
conversion happens transparently.
### Parent seed backup migration
If your vault contains a `parent_seed.enc` file that was encrypted with Fernet,
SeedPass performs a similar upgrade. Upon loading the vault, the application
decrypts the old file, re‑encrypts it with AES‑GCM, and writes the result back to
`parent_seed.enc`. The legacy Fernet file is preserved as
`parent_seed.enc.fernet` so you can revert if needed. No manual steps are
required – simply unlock your vault and the conversion runs automatically.
@@ -1,29 +0,0 @@
# Packaging the GUI with Briefcase
This project uses [BeeWare's Briefcase](https://beeware.org) to generate
platform‑native installers. Once your development environment is set up,
package the GUI by running the following commands from the repository root:
```bash
# Create the application scaffold for your platform
briefcase create
# Compile dependencies and produce a distributable bundle
briefcase build
# Run the packaged application
briefcase run
```
## Command Overview
- **`briefcase create`** — generates the project scaffold for your
operating system. Run this once per platform.
- **`briefcase build`** — compiles dependencies and produces the
distributable bundle.
- **`briefcase run`** — launches the packaged application so you can test
it locally.
After the initial creation step you can repeatedly run `briefcase build`
followed by `briefcase run` to test your packaged application on Windows,
macOS or Linux.
@@ -1,165 +0,0 @@
# BeeWare GUI Adapter
SeedPass ships with a proof-of-concept graphical interface built using [BeeWare](https://beeware.org). The GUI interacts with the same core services as the CLI by instantiating wrappers around `PasswordManager`.
## Getting Started with the GUI
After installing the project dependencies, launch the desktop interface with one
of the following commands:
```bash
seedpass gui
python -m seedpass_gui
seedpass-gui
```
GUI dependencies are optional. Install them alongside SeedPass with:
```bash
pip install "seedpass[gui]"
# or when working from a local checkout
pip install -e .[gui]
```
After installing the optional GUI extras, add the BeeWare backend for your
platform:
```bash
# Linux
pip install toga-gtk
# If installation fails with cairo errors, install libcairo2-dev or the
# cairo development package using your distro's package manager.
# Windows
pip install toga-winforms
# macOS
pip install toga-cocoa
```
The GUI shares the same encrypted vault and configuration as the command line tool.
To generate a packaged binary, run `briefcase build` (after the initial `briefcase create`).
```mermaid
graph TD
core["seedpass.core"]
cli["CLI"]
api["FastAPI server"]
gui["BeeWare GUI"]
ext["Browser Extension"]
cli --> core
gui --> core
api --> core
ext --> api
```
## VaultService and EntryService
`VaultService` provides thread-safe access to vault operations like exporting, importing, unlocking and locking the vault. `EntryService` exposes methods for listing, searching and modifying entries. Both classes live in `seedpass.core.api` and hold a `PasswordManager` instance protected by a `threading.Lock` to ensure safe concurrent access.
```python
class VaultService:
"""Thread-safe wrapper around vault operations."""
def __init__(self, manager: PasswordManager) -> None:
self._manager = manager
self._lock = Lock()
```
```python
class EntryService:
"""Thread-safe wrapper around entry operations."""
def __init__(self, manager: PasswordManager) -> None:
self._manager = manager
self._lock = Lock()
```
## BeeWare Windows
The GUI defines two main windows in `src/seedpass_gui/app.py`. `LockScreenWindow` prompts for the master password and then opens `MainWindow` to display the vault entries.
```python
class LockScreenWindow(toga.Window):
"""Window prompting for the master password."""
def __init__(self, app: SeedPassApp, vault: VaultService, entries: EntryService) -> None:
super().__init__("Unlock Vault")
self.app = app
self.vault = vault
self.entries = entries
...
```
```python
class MainWindow(toga.Window):
"""Main application window showing vault entries."""
def __init__(self, app: SeedPassApp, vault: VaultService, entries: EntryService) -> None:
super().__init__("SeedPass")
self.app = app
self.vault = vault
self.entries = entries
...
```
Each window receives the service instances and calls methods such as `vault.unlock()` or `entries.add_entry()` when buttons are pressed. This keeps the UI thin while reusing the core logic.
## Asynchronous Synchronization
`PasswordManager` performs network synchronization with Nostr using `asyncio`. Methods like `start_background_vault_sync()` create a coroutine that calls `sync_vault_async()` in a background thread or task without blocking the UI.
```python
async def sync_vault_async(self, alt_summary: str | None = None) -> dict[str, list[str] | str] | None:
"""Publish the current vault contents to Nostr and return event IDs."""
...
```
```python
def start_background_vault_sync(self, alt_summary: str | None = None) -> None:
if getattr(self, "offline_mode", False):
return
def _worker() -> None:
asyncio.run(self.sync_vault_async(alt_summary=alt_summary))
try:
loop = asyncio.get_running_loop()
except RuntimeError:
threading.Thread(target=_worker, daemon=True).start()
else:
asyncio.create_task(self.sync_vault_async(alt_summary=alt_summary))
```
This approach ensures synchronization happens asynchronously whether the GUI is running inside or outside an existing event loop.
## Relay Manager and Status Bar
The *Relays* button opens a dialog for adding or removing Nostr relay URLs. The
status bar at the bottom of the main window shows when the last synchronization
completed. It updates automatically when `sync_started` and `sync_finished`
events are published on the internal pubsub bus.
When a ``vault_locked`` event is emitted, the GUI automatically returns to the
lock screen so the session can be reopened with the master password.
## Event Handling
The GUI subscribes to a few core events so the interface reacts automatically when the vault changes state. When `MainWindow` is created it registers callbacks for `sync_started`, `sync_finished` and `vault_locked` on the global pubsub `bus`:
```python
bus.subscribe("sync_started", self.sync_started)
bus.subscribe("sync_finished", self.sync_finished)
bus.subscribe("vault_locked", self.vault_locked)
```
Each handler updates the status bar or returns to the lock screen. The `cleanup` method removes these hooks when the window closes:
```python
def cleanup(self, *args: object, **kwargs: object) -> None:
bus.unsubscribe("sync_started", self.sync_started)
bus.unsubscribe("sync_finished", self.sync_finished)
bus.unsubscribe("vault_locked", self.vault_locked)
```
The [TOTP window](../../02-api_reference.md#totp) demonstrates how such events keep the UI fresh: it shows live two-factor codes that reflect the latest vault data after synchronization.
-610
View File
@@ -1,610 +0,0 @@
# SeedPass
**SeedPass** is a secure password generator and manager built on **Bitcoin's BIP-85 standard**. It uses deterministic key derivation to generate **passwords that are never stored**, but can be easily regenerated when needed. By integrating with the **Nostr network**, SeedPass compresses your encrypted vault and splits it into 50 KB chunks. Each chunk is published as a parameterised replaceable event (`kind 30071`), with a manifest (`kind 30070`) describing the snapshot and deltas (`kind 30072`) capturing changes between snapshots. This allows secure password recovery across devices without exposing your data.
[Tip Jar](https://nostrtipjar.netlify.app/?n=npub16y70nhp56rwzljmr8jhrrzalsx5x495l4whlf8n8zsxww204k8eqrvamnp)
---
**⚠️ Disclaimer**
This software was not developed by an experienced security expert and should be used with caution. There may be bugs and missing features. Each vault chunk is limited to 50 KB and SeedPass periodically publishes a new snapshot to keep accumulated deltas small. The security of the program's memory management and logs has not been evaluated and may leak sensitive information. Loss or exposure of the parent seed places all derived passwords, accounts, and other artifacts at risk.
**🚨 Breaking Change**
Recent releases derive passwords and other artifacts using a fully deterministic algorithm that behaves consistently across Python versions. This improvement means artifacts generated with earlier versions of SeedPass will not match those produced now. Regenerate any previously derived data or retain the old version if you need to reproduce older passwords or keys.
---
### Supported OS
✔ Windows 10/11 • macOS 12+ • Any modern Linux
SeedPass now uses the `portalocker` library for cross-platform file locking. No WSL or Cygwin required.
```mermaid
graph TD
core(seedpass.core)
cli(CLI/TUI)
gui(BeeWare GUI)
ext(Browser extension)
cli --> core
gui --> core
ext --> core
```
SeedPass uses a modular design with a single core library that handles all
security-critical logic. The current CLI/TUI adapter communicates with
`seedpass.core`, and future interfaces like a BeeWare GUI and a browser
extension can hook into the same layer. This architecture keeps the codebase
maintainable while enabling a consistent experience on multiple platforms.
## Table of Contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [1. Clone the Repository](#1-clone-the-repository)
- [2. Create a Virtual Environment](#2-create-a-virtual-environment)
- [3. Activate the Virtual Environment](#3-activate-the-virtual-environment)
- [4. Install Dependencies](#4-install-dependencies)
- [Usage](#usage)
- [Running the Application](#running-the-application)
- [Managing Multiple Seeds](#managing-multiple-seeds)
- [Additional Entry Types](#additional-entry-types)
- [Recovery](#recovery)
- [Security Considerations](#security-considerations)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
## Features
- **Deterministic Password Generation:** Utilize BIP-85 for generating deterministic and secure passwords.
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50 KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas. The manifest's `delta_since` field stores the UNIX timestamp of the latest delta event.
- **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
- **Nested Managed Account Seeds:** SeedPass can derive nested managed account seeds.
- **Interactive TUI:** Navigate through menus to add, retrieve, and modify entries as well as configure Nostr settings.
- **SeedPass 2FA:** Generate TOTP codes with a real-time countdown progress bar.
- **2FA Secret Issuance & Import:** Derive new TOTP secrets from your seed or import existing `otpauth://` URIs.
- **Export 2FA Codes:** Save all stored TOTP entries to an encrypted JSON file for use with other apps.
- **Display TOTP Codes:** Show all active 2FA codes with a countdown timer.
- **Optional External Backup Location:** Configure a second directory where backups are automatically copied.
- **Auto‑Lock on Inactivity:** Vault locks after a configurable timeout for additional security.
- **Quick Unlock:** Optionally skip the password prompt after verifying once. Startup delay is unaffected.
- **Secret Mode:** Copy retrieved passwords directly to your clipboard and automatically clear it after a delay.
- **Tagging Support:** Organize entries with optional tags and find them quickly via search.
- **Typed Search Results:** Searches display each entry's type for easier scanning.
- **Manual Vault Export/Import:** Create encrypted backups or restore them using the CLI or API.
- **Parent Seed Backup:** Securely save an encrypted copy of the master seed.
- **Manual Vault Locking:** Instantly clear keys from memory when needed.
- **Vault Statistics:** View counts for entries and other profile metrics.
- **Change Master Password:** Rotate your encryption password at any time.
- **Checksum Verification Utilities:** Verify or regenerate the script checksum.
- **Relay Management:** List, add, remove or reset configured Nostr relays.
- **Offline Mode (default):** SeedPass runs without network sync until you explicitly enable it.
## Prerequisites
- **Python 3.8+** (3.11 or 3.12 recommended): Install Python from [python.org](https://www.python.org/downloads/) and be sure to check **"Add Python to PATH"** during setup. Using Python 3.13 is currently discouraged because some dependencies do not ship wheels for it yet, which can cause build failures on Windows unless you install the Visual C++ Build Tools.
*Windows only:* Install the [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and select the **C++ build tools** workload.
## Installation
### Quick Installer
Use the automated installer to download SeedPass and its dependencies in one step.
If GTK packages are missing, the installer will try to install them using your
system's package manager (`apt`, `yum`, `pacman`, or Homebrew).
**Linux and macOS:**
```bash
bash -c "$(curl -sSL https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/install.sh)"
```
*Install the beta branch:*
```bash
bash -c "$(curl -sSL https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/install.sh)" _ -b beta
```
**Windows (PowerShell):**
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; $scriptContent = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/install.ps1'); & ([scriptblock]::create($scriptContent))
```
Before running the script, install **Python 3.11** or **3.12** from [python.org](https://www.python.org/downloads/windows/) and tick **"Add Python to PATH"**. You should also install the [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with the **C++ build tools** workload so dependencies compile correctly.
The Windows installer will attempt to install Git automatically if it is not already available. It also tries to
install Python 3 using `winget`, `choco`, or `scoop` when Python is missing and recognizes the `py` launcher if `python`
isn't on your PATH. If these tools are unavailable you'll see a link to download Python directly from
<https://www.python.org/downloads/windows/>. When Python 3.13 or newer is detected without the Microsoft C++ build tools,
the installer now attempts to download Python 3.12 automatically so you don't have to compile packages from source.
**Note:** If this fallback fails, install Python 3.12 manually or install the [Microsoft Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and rerun the installer.
#### Installer Dependency Checks
The installer verifies that core build tooling—C/C++ build tools, Rust, CMake, and the imaging/GTK libraries—are available before completing. Pass `--no-gui` to skip installing GUI packages. On Linux, ensure `xclip` or `wl-clipboard` is installed for clipboard support.
### Uninstall
Run the matching uninstaller if you need to remove a previous installation or clean up an old `seedpass` command:
**Linux and macOS:**
```bash
bash -c "$(curl -sSL https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/uninstall.sh)"
```
If the script warns that it couldn't remove an executable, delete that file manually.
**Windows (PowerShell):**
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; $scriptContent = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/uninstall.ps1'); & ([scriptblock]::create($scriptContent))
```
*Install the beta branch:*
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; $scriptContent = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/PR0M3TH3AN/SeedPass/main/scripts/install.ps1'); & ([scriptblock]::create($scriptContent)) -Branch beta
```
### Manual Setup
Follow these steps to set up SeedPass on your local machine.
### 1. Clone the Repository
First, clone the SeedPass repository from GitHub:
```bash
git clone https://github.com/PR0M3TH3AN/SeedPass.git
```
Navigate to the project directory:
```bash
cd SeedPass
```
### 2. Create a Virtual Environment
It's recommended to use a virtual environment to manage your project's dependencies. Create a virtual environment named `venv`:
```bash
python3 -m venv venv
```
### 3. Activate the Virtual Environment
Activate the virtual environment using the appropriate command for your operating system.
- **On Linux and macOS:**
```bash
source venv/bin/activate
```
- **On Windows:**
```bash
venv\Scripts\activate
```
Once activated, your terminal prompt should be prefixed with `(venv)` indicating that the virtual environment is active.
### 4. Install Dependencies
Install the required Python packages and build dependencies using `pip`.
When upgrading pip, use `python -m pip` inside the virtual environment so that pip can update itself cleanly:
```bash
python -m pip install --upgrade pip
python -m pip install --require-hashes -r requirements.lock
python -m pip install -e .
```
#### Linux Clipboard Support
On Linux, `pyperclip` relies on external utilities like `xclip` or `xsel`.
SeedPass does not install these tools automatically. To use clipboard features
such as secret mode, install **xclip** manually:
```bash
sudo apt install xclip
```
After installing `xclip`, restart SeedPass to enable clipboard support.
## Quick Start
After installing dependencies, activate your virtual environment and install
the package so the `seedpass` command is available, then launch SeedPass and
create a backup:
```bash
# Start the application
seedpass
# Export your index
seedpass vault export --file "~/seedpass_backup.json"
# Later you can restore it
seedpass vault import --file "~/seedpass_backup.json"
# Import also performs a Nostr sync to pull any changes
# Quickly find or retrieve entries
seedpass search "github"
seedpass search --tags "work,personal"
seedpass get "github"
# Search results show the entry type, e.g. "1: Password - GitHub"
# Retrieve a TOTP entry
seedpass entry get "email"
# The code is printed and copied to your clipboard
# Sort or filter the list view
seedpass list --sort label
seedpass list --filter totp
# Use the **Settings** menu to configure an extra backup directory
# on an external drive.
```
For additional command examples, see [01-getting-started/01-advanced_cli.md](01-getting-started/01-advanced_cli.md).
Details on the REST API can be found in [01-getting-started/02-api_reference.md](01-getting-started/02-api_reference.md).
### Vault JSON Layout
The encrypted index file `seedpass_entries_db.json.enc` begins with `schema_version` `2` and stores an `entries` map keyed by entry numbers.
```json
{
"schema_version": 2,
"entries": {
"0": {
"label": "example.com",
"length": 8,
"type": "password",
"notes": ""
}
}
}
```
## Usage
After successfully installing the dependencies, launch the interactive TUI with:
```bash
seedpass
```
You can also run directly from the repository using:
```bash
python src/main.py
```
You can explore other CLI commands using:
```bash
seedpass --help
```
For a full list of commands see [01-getting-started/01-advanced_cli.md](01-getting-started/01-advanced_cli.md). The REST API is described in [01-getting-started/02-api_reference.md](01-getting-started/02-api_reference.md).
### Running the Application
1. **Start the Application:**
```bash
seedpass
```
*(or `python src/main.py` if running directly from the repository)*
2. **Follow the Prompts:**
- **Seed Profile Selection:** If you have existing seed profiles, you'll be prompted to select one or add a new one.
- **Enter Your Password:** This password is crucial as it is used to encrypt and decrypt your parent seed and seed index data.
- **Select an Option:** Navigate through the menu by entering the number corresponding to your desired action.
Example menu:
```bash
Select an option:
1. Add Entry
2. Retrieve Entry
3. Search Entries
4. List Entries
5. Modify an Existing Entry
6. 2FA Codes
7. Settings
Enter your choice (1-7) or press Enter to exit:
```
When choosing **Add Entry**, you can now select from:
- **Password**
- **2FA (TOTP)**
- **SSH Key**
- **Seed Phrase**
- **Nostr Key Pair**
- **PGP Key**
- **Key/Value**
- **Managed Account**
### Adding a Password Entry
After selecting **Password**, SeedPass asks you to choose a mode:
1. **Quick** – enter only a label, username, URL, desired length, and whether to include special characters. All other fields use defaults.
2. **Advanced** – continue through prompts for notes, tags, custom fields, and detailed password policy settings.
Both modes generate the password, display it (or copy it to the clipboard in Secret Mode), and save the entry to your encrypted vault.
### Adding a 2FA Entry
1. From the main menu choose **Add Entry** and select **2FA (TOTP)**.
2. Pick **Make 2FA** to derive a new secret from your seed or **Import 2FA** to paste an existing `otpauth://` URI or secret.
3. Provide a label for the account (for example, `GitHub`).
4. SeedPass automatically chooses the next available derivation index when deriving.
5. Optionally specify the TOTP period and digit count.
6. SeedPass displays the URI and secret, along with a QR code you can scan to import it into your authenticator app.
### Modifying a 2FA Entry
1. From the main menu choose **Modify an Existing Entry** and enter the index of the 2FA code you want to edit.
2. SeedPass will show the current label, period, digit count, and archived status.
3. Enter new values or press **Enter** to keep the existing settings.
4. When retrieving a 2FA entry you can press **E** to edit the label, period or digit count, or **A** to archive/unarchive it.
5. The updated entry is saved back to your encrypted vault.
6. Archived entries are hidden from lists but can be viewed or restored from the **List Archived** menu.
7. When editing an archived entry you'll be prompted to restore it after saving your changes.
### Using Secret Mode
When **Secret Mode** is enabled, SeedPass copies retrieved passwords directly to your clipboard instead of displaying them on screen. The clipboard clears automatically after the delay you choose.
1. From the main menu open **Settings** and select **Toggle Secret Mode**.
2. Choose how many seconds to keep passwords on the clipboard.
3. Retrieve an entry and SeedPass will confirm the password was copied.
### Additional Entry Types
SeedPass supports storing more than just passwords and 2FA secrets. You can also create entries for:
- **SSH Key** – deterministically derive an Ed25519 key pair for servers or git hosting platforms.
- **Seed Phrase** – store only the BIP-85 index and word count. The mnemonic is regenerated on demand.
- **PGP Key** – derive an OpenPGP key pair from your master seed.
- **Nostr Key Pair** – store the index used to derive an `npub`/`nsec` pair for Nostr clients.
When you retrieve one of these entries, SeedPass can display QR codes for the
keys. The `npub` is wrapped in the `nostr:` URI scheme so any client can scan
it, while the `nsec` QR is shown only after a security warning.
- **Key/Value** – store a simple key and value for miscellaneous secrets or configuration data.
- **Managed Account** – derive a child seed under the current profile. Loading a managed account switches to a nested profile and the header shows `<parent_fp> > Managed Account > <child_fp>`. Press Enter on the main menu to return to the parent profile.
The table below summarizes the extra fields stored for each entry type. Every
entry includes a `label`, while only password entries track a `url`.
| Entry Type | Extra Fields |
|---------------|---------------------------------------------------------------------------------------------------------------------------------------|
| Password | `username`, `url`, `length`, `archived`, optional `notes`, optional `custom_fields` (may include hidden fields), optional `tags` |
| 2FA (TOTP) | `index` or `secret`, `period`, `digits`, `archived`, optional `notes`, optional `tags` |
| SSH Key | `index`, `archived`, optional `notes`, optional `tags` |
| Seed Phrase | `index`, `word_count` *(mnemonic regenerated; never stored)*, `archived`, optional `notes`, optional `tags` |
| PGP Key | `index`, `key_type`, `archived`, optional `user_id`, optional `notes`, optional `tags` |
| Nostr Key Pair| `index`, `archived`, optional `notes`, optional `tags` |
| Key/Value | `key`, `value`, `archived`, optional `notes`, optional `custom_fields`, optional `tags` |
| Managed Account | `index`, `word_count`, `fingerprint`, `archived`, optional `notes`, optional `tags` |
### Managing Multiple Seeds
SeedPass allows you to manage multiple seed profiles (previously referred to as "fingerprints"). Each seed profile has its own parent seed and associated data, enabling you to compartmentalize your passwords.
- **Add a New Seed Profile:**
- From the main menu, select **Settings** then **Profiles** and choose "Add a New Seed Profile".
- Choose to paste in a full seed, enter one word at a time, or generate a new seed.
- When entering a seed word by word, each word is hidden with `*` and the screen refreshes after every entry for clarity. You'll review the completed phrase after the last word and can correct mistakes before it is saved.
- If generating a new seed, you'll be provided with a 12-word BIP-85 seed phrase. **Ensure you write this down and store it securely.**
- **Switch Between Seed Profiles:**
- From the **Profiles** menu, select "Switch Seed Profile".
- You'll see a list of available seed profiles.
- Enter the number corresponding to the seed profile you wish to switch to.
- Enter the master password associated with that seed profile.
- **List All Seed Profiles:**
- In the **Profiles** menu, choose "List All Seed Profiles" to view all existing profiles.
- **Set Seed Profile Name:**
- In the **Profiles** menu, choose "Set Seed Profile Name" to assign a label to the current profile. The name is stored locally and shown next to the fingerprint.
**Note:** The term "seed profile" is used to represent different sets of seeds you can manage within SeedPass. This provides an intuitive way to handle multiple identities or sets of passwords.
### Recovery
If you previously backed up your vault to Nostr you can restore it during the
initial setup. You must provide both your 12 -word master seed and the master
password that encrypted the vault; without the correct password the retrieved
data cannot be decrypted.
1. Start SeedPass and choose option **4** when prompted to set up a seed.
2. Paste your BIP‑85 seed phrase when asked.
3. Enter the master password associated with that seed.
4. SeedPass initializes the profile and attempts to download the encrypted
vault from the configured relays.
5. A success message confirms the vault was restored. If no data is found a
failure message is shown and a new empty vault is created.
### Configuration File and Settings
SeedPass keeps per-profile settings in an encrypted file named `seedpass_config.json.enc` inside each profile directory under `~/.seedpass/`. This file stores your chosen Nostr relays and the optional settings PIN. New profiles start with the following default relays:
```
wss://relay.snort.social
wss://nostr.oxtr.dev
wss://relay.primal.net
```
You can manage your relays and sync with Nostr from the **Settings** menu:
1. From the main menu choose `6` (**Settings**).
2. Select `2` (**Nostr**) to open the Nostr submenu.
3. Choose `1` to back up your encrypted index to Nostr.
4. Select `2` to restore the index from Nostr.
5. Choose `3` to view your current relays.
6. Select `4` to add a new relay URL.
7. Choose `5` to remove a relay by number.
8. Select `6` to reset to the default relay list.
9. Choose `7` to display your Nostr public key.
10. Select `8` to return to the Settings menu.
Back in the Settings menu you can:
* Select `3` to change your master password.
* Choose `4` to verify the script checksum.
* Select `5` to generate a new script checksum.
* Choose `6` to back up the parent seed.
* Select `7` to export the database to an encrypted file.
* Choose `8` to import a database from a backup file. This also performs a Nostr sync automatically.
* Select `9` to export all 2FA codes.
* Choose `10` to set an additional backup location. A backup is created
immediately after the directory is configured.
* Select `11` to change the inactivity timeout.
* Choose `12` to lock the vault and require re-entry of your password.
* Select `13` to view seed profile stats. The summary lists counts for
passwords, TOTP codes, SSH keys, seed phrases, and PGP keys. It also shows
whether both the encrypted database and the script itself pass checksum
validation.
* Choose `14` to toggle Secret Mode and set the clipboard clear delay.
* Select `15` to toggle Offline Mode. SeedPass starts offline; disable it here to enable Nostr syncing.
* Choose `16` to toggle Quick Unlock so subsequent actions skip the password prompt. Startup delay is unchanged.
* Select `17` to return to the main menu.
## Running Tests
SeedPass includes a small suite of unit tests located under `src/tests`. **Before running `pytest`, be sure to install the test requirements.** Activate your virtual environment and run `pip install --require-hashes -r requirements.lock` to ensure all testing dependencies are available. Then run the tests with **pytest**. Use `-vv` to see INFO-level log messages from each passing test:
```bash
pip install --require-hashes -r requirements.lock
pytest -vv
```
`test_fuzz_key_derivation.py` uses Hypothesis to generate random passwords,
seeds and configuration data. It performs round-trip encryption tests with the
`EncryptionManager` to catch edge cases automatically. These fuzz tests run in
CI alongside the rest of the suite.
### Exploring Nostr Index Size Limits
`test_nostr_index_size.py` demonstrates how SeedPass rotates snapshots after too many delta events.
Each chunk is limited to 50 KB, so the test gradually grows the vault to observe
when a new snapshot is triggered. Use the `NOSTR_TEST_DELAY` environment
variable to control the delay between publishes when experimenting with large vaults.
```bash
pytest -vv -s -n 0 src/tests/test_nostr_index_size.py --desktop --max-entries=1000
```
### Generating a Test Profile
Use the helper script below to populate a profile with sample entries for testing:
```bash
python scripts/generate_test_profile.py --profile demo_profile --count 100
```
The script determines the fingerprint from the generated seed and stores the
vault under `~/.seedpass/tests/<fingerprint>`. SeedPass only discovers profiles
inside `~/.seedpass/`, so copy the fingerprint directory out of the `tests`
subfolder (or adjust `APP_DIR` in `constants.py`) if you want to use the
generated seed with the main application. The fingerprint is printed after
creation and the encrypted index is published to Nostr. Use that same seed
phrase to load SeedPass. The app checks Nostr on startup and pulls any newer
snapshot so your vault stays in sync across machines. Synchronization also runs
in the background after unlocking or when switching profiles.
### Automatically Updating the Script Checksum
SeedPass stores a SHA-256 checksum for the main program in `~/.seedpass/seedpass_script_checksum.txt`.
To keep this value in sync with the source code, install the pre‑push git hook:
```bash
pre-commit install -t pre-push
```
After running this command, every `git push` will execute `scripts/update_checksum.py`,
updating the checksum file automatically.
If the checksum file is missing, generate it manually:
```bash
python scripts/update_checksum.py
```
If SeedPass reports a "script checksum mismatch" warning on startup,
regenerate the checksum with `seedpass util update-checksum` or select
"Generate Script Checksum" from the Settings menu.
To run mutation tests locally, generate coverage data first and then execute `mutmut`:
```bash
pytest --cov=src src/tests
python -m mutmut run --paths-to-mutate src --tests-dir src/tests --runner "python -m pytest -q" --use-coverage --no-progress
python -m mutmut results
```
Mutation testing is disabled in the GitHub workflow due to reliability issues and should be run on a desktop environment instead.
## Security Considerations
**Important:** The password you use to encrypt your parent seed is also required to decrypt the seed index data retrieved from Nostr. **It is imperative to remember this password** and be sure to use it with the same seed, as losing it means you won't be able to access your stored index. Secure your 12-word seed **and** your master password.
- **Backup Your Data:** Regularly back up your encrypted data and checksum files to prevent data loss.
- **Backup the Settings PIN:** Your settings PIN is stored in the encrypted configuration file. Keep a copy of this file or remember the PIN, as losing it will require deleting the file and reconfiguring your relays.
- **Protect Your Passwords:** Do not share your master password or seed phrases with anyone and ensure they are strong and unique.
- **Backing Up the Parent Seed:** Use the CLI `vault reveal-parent-seed` command or the `/api/v1/vault/backup-parent-seed` endpoint with explicit confirmation to create an encrypted backup. The API does not return the seed directly.
- **No PBKDF2 Salt Needed:** SeedPass deliberately omits an explicit PBKDF2 salt. Every password is derived from a unique 512-bit BIP-85 child seed, which already provides stronger per-password uniqueness than a conventional 128-bit salt.
- **Checksum Verification:** Always verify the script's checksum to ensure its integrity and protect against unauthorized modifications.
- **Potential Bugs and Limitations:** Be aware that the software may contain bugs and lacks certain features. Snapshot chunks are capped at 50 KB and the client rotates snapshots after enough delta events accumulate. The security of memory management and logs has not been thoroughly evaluated and may pose risks of leaking sensitive information.
- **Multiple Seeds Management:** While managing multiple seeds adds flexibility, it also increases the responsibility to secure each seed and its associated password.
- **No PBKDF2 Salt Required:** SeedPass deliberately omits an explicit PBKDF2 salt. Every password is derived from a unique 512-bit BIP-85 child seed, which already provides stronger per-password uniqueness than a conventional 128-bit salt.
- **Default KDF Iterations:** New profiles start with 50,000 PBKDF2 iterations. Use `seedpass config set kdf_iterations` to change this.
- **Offline Mode (default):** Nostr sync is disabled until you explicitly enable it via the Settings menu or `seedpass config toggle-offline`.
- **Quick Unlock:** Store a hashed copy of your password so future actions skip the prompt. Startup delay no longer changes. Use with caution on shared systems.
## Contributing
Contributions are welcome! If you have suggestions for improvements, bug fixes, or new features, please follow these steps:
1. **Fork the Repository:** Click the "Fork" button on the top right of the repository page.
2. **Create a Branch:** Create a new branch for your feature or bugfix.
```bash
git checkout -b feature/YourFeatureName
```
3. **Commit Your Changes:** Make your changes and commit them with clear messages.
```bash
git commit -m "Add feature X"
```
4. **Push to GitHub:** Push your changes to your forked repository.
```bash
git push origin feature/YourFeatureName
```
5. **Create a Pull Request:** Navigate to the original repository and create a pull request describing your changes.
## License
This project is licensed under the [MIT License](LICENSE). See the [LICENSE](LICENSE) file for details.
## Contact
For any questions, suggestions, or support, please open an issue on the [GitHub repository](https://github.com/PR0M3TH3AN/SeedPass/issues) or contact the maintainer directly on [Nostr](https://primal.net/p/npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx).
---
*Stay secure and keep your passwords safe with SeedPass!*
---
-11
View File
@@ -1,11 +0,0 @@
{
"name": "docs",
"private": true,
"scripts": {
"dev": "eleventy --serve",
"build": "node node_modules/archivox/src/generator/index.js"
},
"dependencies": {
"archivox": "^1.0.0"
}
}
-3
View File
@@ -1,3 +0,0 @@
[build]
command = "node build-docs.js"
publish = "_site"
-6357
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
{
"name": "archivox",
"version": "1.0.0",
"description": "Archivox static site generator",
"scripts": {
"dev": "eleventy --serve",
"build": "node src/generator/index.js",
"test": "jest"
},
"dependencies": {
"@11ty/eleventy": "^2.0.1",
"gray-matter": "^4.0.3",
"marked": "^11.1.1",
"lunr": "^2.3.9",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"jest": "^29.6.1",
"puppeteer": "^24.12.1"
},
"license": "MIT",
"bin": {
"create-archivox": "./bin/create-archivox.js"
}
}
-7
View File
@@ -1,7 +0,0 @@
module.exports = {
onPageRendered: async ({ html, file }) => {
// Example: inject analytics script into each page
const snippet = '\n<script>console.log("Page viewed: ' + file + '")</script>';
return { html: html.replace('</body>', `${snippet}</body>`) };
}
};
-6
View File
@@ -1,6 +0,0 @@
site:
title: "Archivox Docs"
description: "Simple static docs."
navigation:
search: true
@@ -1,3 +0,0 @@
# Install
Run `npm install` then `npm run build` to generate your site.
@@ -1,3 +0,0 @@
# Getting Started
This section helps you begin with Archivox.
-3
View File
@@ -1,3 +0,0 @@
# Welcome to Archivox
This is your new documentation site. Start editing files in the `content/` folder.
-11
View File
@@ -1,11 +0,0 @@
{
"name": "my-archivox-site",
"private": true,
"scripts": {
"dev": "eleventy --serve",
"build": "node node_modules/archivox/src/generator/index.js"
},
"dependencies": {
"archivox": "*"
}
}
-23
View File
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="{% if config.theme.darkMode %}dark{% else %}light{% endif %}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title | default(config.site.title) }}</title>
<link rel="stylesheet" href="/assets/theme.css" />
</head>
<body>
{% include "partials/header.njk" %}
<div id="sidebar-overlay" class="sidebar-overlay"></div>
<div class="container">
{% include "partials/sidebar.njk" %}
<main id="content">
<nav id="breadcrumbs" class="breadcrumbs"></nav>
{{ content | safe }}
</main>
</div>
{% include "partials/footer.njk" %}
<script src="/assets/lunr.js"></script>
<script src="/assets/theme.js"></script>
</body>
</html>
-14
View File
@@ -1,14 +0,0 @@
<footer class="footer">
{% if config.footer.links %}
<nav class="footer-links">
{% for link in config.footer.links %}
<a href="{{ link.url }}">{{ link.text }}</a>
{% endfor %}
</nav>
{% endif %}
<p>&copy; {{ config.site.title }}</p>
<div class="footer-permanent-links">
<a href="https://github.com/PR0M3TH3AN/Archivox">GitHub</a>
<a href="https://nostrtipjar.netlify.app/?n=npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx">Tip Jar</a>
</div>
</footer>
-7
View File
@@ -1,7 +0,0 @@
<header class="header">
<button id="sidebar-toggle" class="sidebar-toggle" aria-label="Toggle navigation">☰</button>
<a href="/" class="logo">{{ config.site.title }}</a>
<input id="search-input" class="search-input" type="search" placeholder="Search..." aria-label="Search" />
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">🌓</button>
<div id="search-results" class="search-results"></div>
</header>
-29
View File
@@ -1,29 +0,0 @@
{% macro renderNav(items, pageUrl) %}
<ul>
{% for item in items %}
<li>
{% if item.children and item.children.length %}
{% set sectionPath = item.path | replace('index.html', '') %}
<details class="nav-section" {% if pageUrl.startsWith(sectionPath) %}open{% endif %}>
<summary>
<a href="{{ item.path }}" class="nav-link{% if item.path === pageUrl %} active{% endif %}">
{{ item.displayName or item.page.title }}
</a>
</summary>
{{ renderNav(item.children, pageUrl) }}
</details>
{% else %}
<a href="{{ item.path }}" class="nav-link{% if item.path === pageUrl %} active{% endif %}">
{{ item.displayName or item.page.title }}
</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
<aside class="sidebar" id="sidebar">
<nav>
{{ renderNav(navigation, page.url) }}
</nav>
</aside>
+243
View File
@@ -0,0 +1,243 @@
/* docs.css - Specific styles for the documentation viewer */
.docs-wrapper {
max-width: 1200px;
/* Wider for docs */
}
/* Base Layout */
.docs-layout {
display: flex;
flex-direction: row;
padding: 20px;
gap: 20px;
min-height: 70vh;
}
@media (max-width: 850px) {
.docs-layout {
flex-direction: column;
}
}
/* Sidebar Navigation */
.docs-sidebar {
width: 250px;
flex-shrink: 0;
margin-bottom: 0;
border-right: 1px dotted var(--border-color);
padding-right: 20px;
padding-bottom: 20px;
}
@media (max-width: 850px) {
.docs-sidebar {
width: 100%;
border-right: none;
border-bottom: 1px dotted var(--border-color);
}
}
.sidebar-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: 15px;
color: var(--text-bright);
letter-spacing: 1px;
padding-bottom: 8px;
border-bottom: 1px dotted var(--border-color);
}
.docs-nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.docs-nav-list a {
display: block;
color: var(--text-muted);
font-size: 0.9rem;
padding: 5px 8px;
border-radius: 3px;
transition: all 0.2s;
border-left: 2px solid transparent;
}
.docs-nav-list a:hover {
color: var(--text-bright);
background: rgba(14, 165, 233, 0.05);
/* very light blue hover */
border-left-color: var(--accent-secondary);
}
.docs-nav-list a.active {
color: var(--text-primary);
font-weight: bold;
background: rgba(14, 165, 233, 0.1);
border-left-color: var(--accent-primary);
}
/* Document Content Area */
.docs-content {
flex-grow: 1;
margin-bottom: 0;
padding-left: 10px;
padding-right: 10px;
overflow-x: hidden;
}
/* Loaders / States */
.loading-state,
.error-state {
padding: 40px;
text-align: center;
color: var(--text-muted);
font-size: 1.1rem;
}
.error-state {
color: var(--accent-danger);
border: 1px dashed var(--accent-danger);
background: rgba(239, 68, 68, 0.05);
}
/* Markdown Rendering Styles */
.markdown-body {
line-height: 1.7;
font-size: 0.95rem;
color: var(--text-primary);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
color: var(--text-bright);
margin-top: 1.5em;
margin-bottom: 0.8em;
font-weight: 700;
}
.markdown-body h1 {
font-size: 2rem;
border-bottom: 1px dotted var(--border-color);
padding-bottom: 0.3em;
color: var(--accent-primary);
margin-top: 0;
}
.markdown-body h2 {
font-size: 1.5rem;
border-bottom: 1px dotted var(--border-color);
padding-bottom: 0.3em;
}
.markdown-body h3 {
font-size: 1.25rem;
}
.markdown-body p,
.markdown-body ul,
.markdown-body ol {
margin-bottom: 1.2em;
}
.markdown-body a {
color: var(--accent-secondary);
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 4px;
}
.markdown-body a:hover {
color: var(--accent-primary);
text-decoration-style: solid;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
}
.markdown-body li {
margin-bottom: 0.5em;
}
.markdown-body blockquote {
margin: 0 0 1.2em 0;
padding: 10px 20px;
color: var(--text-muted);
border-left: 4px solid var(--accent-secondary);
background: #0b0f13;
border-radius: 0 4px 4px 0;
}
/* Override default highlight.js to match our theme */
.markdown-body pre {
background: #0b0f13 !important;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 15px;
overflow-x: auto;
margin-bottom: 1.2em;
}
.markdown-body pre code {
background: transparent !important;
padding: 0;
font-size: 0.9rem;
font-family: var(--font-mono);
color: var(--text-bright);
}
/* Inline code blocks */
.markdown-body :not(pre)>code {
background: rgba(255, 255, 255, 0.05);
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
color: var(--accent-secondary);
border: 1px solid var(--border-highlight);
}
/* Tables */
.markdown-body table {
width: 100%;
margin-bottom: 1.5em;
border-collapse: collapse;
}
.markdown-body table th,
.markdown-body table td {
padding: 10px 15px;
border: 1px solid var(--border-color);
text-align: left;
}
.markdown-body table th {
background: rgba(14, 165, 233, 0.05);
color: var(--text-bright);
font-weight: bold;
}
/* Horizontal Rule */
.markdown-body hr {
height: 1px;
border: none;
background: var(--border-color);
margin: 2em 0;
}
/* Images */
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: 4px;
border: 1px solid var(--border-color);
}
+87 -101
View File
@@ -1,105 +1,91 @@
// Dark Mode Toggle with Persistence
const toggleCheckbox = document.getElementById('dark-mode-checkbox');
document.addEventListener('DOMContentLoaded', () => {
// --- Boot Sequence Animation ---
const bootLines = [
"INITIALIZING SYSTEM MODULES...",
"KERNEL BOOT... [OK]",
"MOUNTING ENCRYPTED FILE SYSTEM... [OK]",
"ESTABLISHING NOSTR RELAY CONNECTION... [OK]",
"VERIFYING BIP-85 DERIVATION TREE... [OK]",
"DECRYPTING VAULT... [OK]",
"SYSTEM READY. LAUNCHING TUI..."
];
// Check for saved user preference
if (localStorage.getItem('dark-mode') === 'enabled') {
toggleCheckbox.checked = true;
document.body.classList.add('dark-mode');
}
const bootTextElement = document.getElementById('boot-text');
const bootSequenceElement = document.getElementById('boot-sequence');
const mainContentElement = document.getElementById('main-content');
toggleCheckbox.addEventListener('change', function() {
if (this.checked) {
document.body.classList.add('dark-mode');
localStorage.setItem('dark-mode', 'enabled');
} else {
document.body.classList.remove('dark-mode');
localStorage.setItem('dark-mode', 'disabled');
let currentLine = 0;
function typeLine() {
if (!bootTextElement) return;
if (currentLine < bootLines.length) {
bootTextElement.textContent += bootLines[currentLine] + "\n";
currentLine++;
// Random delay between lines to simulate processing (50ms to 250ms)
const delay = Math.random() * 200 + 50;
setTimeout(typeLine, delay);
} else {
// End boot sequence
setTimeout(() => {
bootSequenceElement.style.opacity = '0';
bootSequenceElement.style.transition = 'opacity 0.5s ease-out';
setTimeout(() => {
bootSequenceElement.style.display = 'none';
if(mainContentElement) {
mainContentElement.style.display = 'flex';
}
}, 500);
}, 600); // Wait 600ms after last line
}
}
// Start boot sequence slightly after load
setTimeout(typeLine, 300);
// --- Copy to Clipboard Functionality ---
const copyBtn = document.getElementById('copy-btn');
const installCmd = document.getElementById('install-cmd');
const copyFeedback = document.getElementById('copy-feedback');
if (copyBtn && installCmd && copyFeedback) {
copyBtn.addEventListener('click', () => {
// Create a temporary textarea to hold the text to copy
const tempTextArea = document.createElement('textarea');
tempTextArea.value = installCmd.textContent;
document.body.appendChild(tempTextArea);
tempTextArea.select();
try {
// Execute the copy command
document.execCommand('copy');
copyFeedback.textContent = '>> Command copied to clipboard <<';
copyFeedback.style.color = 'var(--accent-primary)';
// Change icon temporarily to checkmark
const icon = copyBtn.querySelector('i');
if (icon) {
icon.className = 'fas fa-check';
}
copyBtn.style.color = 'var(--accent-primary)';
// Reset after 2 seconds
setTimeout(() => {
copyFeedback.textContent = '';
if (icon) {
icon.className = 'far fa-copy';
}
copyBtn.style.color = '';
}, 2000);
} catch (err) {
copyFeedback.textContent = '[ERROR: Failed to copy]';
copyFeedback.style.color = 'var(--accent-danger)';
}
// Remove the temporary textarea
document.body.removeChild(tempTextArea);
});
}
});
// Responsive Navbar Toggle
const menuToggle = document.querySelector('.menu-toggle');
const navLinks = document.querySelector('.nav-links');
if (menuToggle) {
menuToggle.addEventListener('click', () => {
navLinks.classList.toggle('active');
});
}
// Active Link Highlighting Based on Scroll
const sections = document.querySelectorAll('section');
const navLinksArray = document.querySelectorAll('.nav-links a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop - 70;
if (pageYOffset >= sectionTop) {
current = section.getAttribute('id');
}
});
navLinksArray.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${current}`) {
link.classList.add('active');
}
});
});
// Smooth Scroll for Older Browsers
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
window.scrollTo({
top: target.offsetTop - 60, // Adjust for navbar height
behavior: 'smooth'
});
}
});
});
// Log when the page is loaded
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-copy-target]').forEach((button) => {
const buttonText = button.querySelector('.copy-button-text');
const buttonIcon = button.querySelector('i');
const status = button.closest('.install-command-shell')?.querySelector('.install-copy-status');
button.addEventListener('click', async () => {
const targetId = button.getAttribute('data-copy-target');
const target = document.getElementById(targetId);
if (!target) return;
const text = target.textContent.trim();
try {
await navigator.clipboard.writeText(text);
} catch (_err) {
const temp = document.createElement('textarea');
temp.value = text;
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
document.body.removeChild(temp);
}
button.classList.add('copied');
if (buttonText) buttonText.textContent = 'Copied';
if (buttonIcon) buttonIcon.className = 'fas fa-check';
if (status) status.textContent = 'Install command copied to clipboard.';
setTimeout(() => {
button.classList.remove('copied');
if (buttonText) buttonText.textContent = 'Copy';
if (buttonIcon) buttonIcon.className = 'fas fa-copy';
if (status) status.textContent = '';
}, 1800);
});
});
console.log('SeedPass landing page loaded');
});
+490 -1102
View File
File diff suppressed because it is too large. Load diff