Compare commits

..
3 Commits
10 changed files with 384 additions and 1655 deletions
-263
View File
@@ -1,263 +0,0 @@
# Zapstore App Stacks — Privacy Review
A systematic privacy and security review of apps in the [Zapstore](https://zapstore.dev) catalog. Each app is cloned from its source repository, scanned for tracking SDKs, permissions, and privacy practices, then assigned a verdict. Approved apps are organized into curated stacks for publication to the Zapstore relay.
## Project Structure
```
.
├── README.md # This file
├── .gitignore
├── plans/ # Planning documents and methodology
│ ├── app-review-methodology.md
│ ├── proposed-categories.md
│ └── review-todo.md
├── scripts/ # Python scripts for review and publishing
│ ├── review_apps.py # Main review script (SSH-based)
│ ├── generate_stacks.py # Map apps to categories, generate stack events
│ ├── publish_stack.py # Sign and publish stacks to relay
│ └── ...
├── data/ # Review results and app data (JSON)
│ ├── review_results_final.json # All 280 apps with verdicts
│ ├── apps_data.json # Zapstore catalog data
│ └── ...
├── reports/ # Human-readable reports (Markdown)
│ ├── review_report_by_category.md # Full category-by-category report
│ ├── final_report.md # Top 2 apps per category
│ └── ...
└── stacks/ # Generated Nostr stack events (kind 30267)
├── all_stacks.json # All 20 stacks in one file
└── ...
```
## Review Results
| Verdict | Count | Meaning |
|---------|-------|---------|
| ✅ **APPROVED** | **175** | No tracking SDKs, minimal/justified permissions, open source |
| ⚠️ **FLAGGED** | **36** | Contains tracking SDKs or excessive permissions |
| ⏭️ **SKIPPED** | **41** | Could not clone repository (no mirror, no GitHub URL) |
| ❓ **UNCLEAR** | **28** | Needs human investigation (app not in catalog, auth required) |
## 20 Curated App Stacks
These stacks are ready to publish to the Zapstore relay as kind-30267 events.
### 1. Bitcoin & Lightning Wallets
Self-custodial Bitcoin and Lightning wallet apps.
- ZEUS, Nunchuk, Cake Wallet, Alby Go, Blitz Wallet, BlueWallet, Electrum, Oubli, MercaSats, Lightning Reaction
### 2. Nostr Clients
Nostr-native social and communication clients.
- Amethyst, Amber, Wisp, Dark Wisp, White Noise, Nospeak, Nostrord, Shosho, TravelTelly, Nmail, Divine, YakiHonne, Nests, Nostria, Zapstore Alpha, PearCal, PearCircle, Ditto, Flotilla
### 3. Secure Messaging
End-to-end encrypted messaging and email apps.
- SimpleX, Conversations, Delta Chat, aTalk, Element, Element X, Thunderbird, FairEmail, Tuta, Quiet, SpamBlocker, Deku SMS
### 4. VPN & Privacy Tools
VPN clients, firewalls, and network privacy tools.
- Orbot, Mullvad VPN, Tailscale, AmneziaVPN, Proton VPN, Rethink, PCAPdroid, WG Tunnel, ByeDPI, NeoStumbler, SD Maid, InviZible Pro, Private DNS Quick Setting
### 5. Password Managers & Auth
Password managers, 2FA authenticators, and identity tools.
- Bitwarden, KeePassDX, Aegis, Authnkey, Ente Auth, Keep, AliasVault, PassVault, LibreFind
### 6. Maps & Navigation
Offline maps, navigation, and location tools.
- Organic Maps, OsmAnd~, StreetComplete, CoMaps, OwnTracks, OSMTracker
### 7. Media Players & Streaming
Video and music players, streaming clients.
- VLC, NewPipe, PipePipe, Auxio, Musify, Metrolist, FreeTube, Zaptrax, Zappix, mpvEx, KurobaEx
### 8. Productivity & Notes
Note-taking, task management, and productivity tools.
- Flux, Notesnook, Quillpad, Saber, DAVx⁵, Super Productivity, Trilium Notes, SilentNotes, Manent, Meiso, Florid, Plektos, Urn, Screen Time, Grit, Numo, timeto.me, Converter NOW
### 9. File Management & Cloud Sync
Cloud storage, file sync, and file management tools.
- Nextcloud, Syncthing-Fork, Seafile, File transfer, primitive ftpd, Paperless Mobile, GitSync, wormhole, SFTP Documents Provider, OSS Document Scanner
### 10. Browsers
Privacy-focused web browsers.
- DuckDuckGo, Cromite
### 11. Social Media
Federated and alternative social media clients.
- Mastodon, Nekogram, Infinity+, NewsBlur, Claw, Polymarket Viewer
### 12. Finance & Budgeting
Personal finance, expense tracking, and budgeting.
- Flow, Pennywise AI Tracker, Dev Stocks Widget, Seeker, PearGuard, Mostro, Shopt
### 13. Health & Fitness
Health tracking, diet, and fitness apps.
- Daily Dozen, Screen Time, Astronia
### 14. Education & Reference
Learning, dictionary, and reference tools.
- freeCodeCamp, freeDictionary, Ciyue, Keyman, NeverTooManyBooks, Calibre Web Companion, Repertoire, ListenBrainz, CPU Info, microMathematics Plus, Mental Math
### 15. Photography & Image Tools
Camera, photo editing, scanning, and gallery apps.
- Image Toolbox, FairScan, PhotoPrism, PicGuard, Gallery apps, YTDLnis, ElCaju, Espy, PDF Wallet
### 16. Utilities & Tools
System utilities, converters, and general-purpose tools.
- Binary Eye, CPU Info, Converter NOW, Catima, BT Remote, Lawnicons, Peristyle, BinEd, MMRL, Canta, Amarok, ServerBox, wX, Mako, Rush, HeliBoard, DeskClock, Redomi, Scrobble, Feeder, SMS2Email, Inure, Unciv
### 17. Games
Open source games across genres.
- Shattered Pixel Dungeon, Wesnoth, Feudal Tactics, Burger Party, ChipDefense, Roboyard, Breakout 71, Damas Clash, Unciv
### 18. Communication (Non-Nostr)
Voice/video calls, remote desktop, and messaging.
- Telegram, RustDesk, Sideband, Meshtastic
### 19. Development Tools
Code editors, Git clients, and developer tools.
- Acode, GitSync, BinEd, freeCodeCamp, GitHub Store, Kai 9000
### 20. Calendar & Scheduling
Calendar apps, scheduling, and time management.
- Calendar by Form*, PearCal, timeto.me, Sidestep
## Review Methodology
Each app is reviewed using a structured process:
1. **Clone** from local Gitea mirror (or GitHub fallback)
2. **Scan AndroidManifest.xml** for requested permissions
3. **Check build.gradle\*** for actual dependency declarations (distinguishing `implementation` vs `playImplementation` vs `compileOnly`)
4. **Verify API calls** — search for actual SDK method invocations (not just dependency names)
5. **Check manifest metadata** that disables analytics
6. **Check build flavors** (Google Play vs F-Droid)
7. **Determine verdict**: ✅ APPROVED or ⚠️ FLAGGED
See [`plans/app-review-methodology.md`](plans/app-review-methodology.md) for full details.
## Publishing
Stack events (kind 30267) are published to `wss://relay.zapstore.dev` using the [`publish_stack.py`](scripts/publish_stack.py) script, which signs via `qrexec` (Qubes OS `nostr_signer`).
## Nostr Kinds Reference
The following Nostr event kinds are relevant to this project:
| Kind | Name | Usage | Status |
|------|------|-------|--------|
| **32267** | Software Application | The app listing event published by developers. Identified by `d` tag (Android package name) and publisher pubkey. Referenced in stacks via `a` tags as `32267:<pubkey>:<identifier>`. | Core data source |
| **30267** | App curation set | Curated collections of apps (stacks). We publish privacy-approved stacks as kind 30267 events with `a` tags referencing each approved app. | Used for output |
| **30078** | App-specific data | Generic parameterized replaceable event. Already used by Zapstore for device state, bookmarks, etc. Candidate for storing per-app review results from LLM agents. | Planned for reviews |
| **3063** | Software Asset | APK metadata including hash, size, version code, platform. Used to track current version info for reviewed apps. | Used for version tracking |
| **30063** | Release artifact set | Group of artifacts for a software release. Links to kind 3063 assets and kind 32267 app events. | Reference |
| **1986** | Relay reviews | Existing review kind, but scoped to Nostr relays rather than software apps. | Not applicable |
See [`references/registry-of-kinds/schema.yaml`](references/registry-of-kinds/schema.yaml) for the full kinds registry and [`references/nips/51.md`](references/nips/51.md) for list/set definitions.
## Kind 30078 Event Data Model
We use **kind 30078** (App-specific data) events for two purposes: app definitions and app reviews. The frontend queries the Nostr relay directly for these events on page load, rather than reading a static file.
### App Definition Events
Each app in the catalog is defined by a kind 30078 event with `#t: app-definition`. These are published when an app is added via the PHP backend.
```jsonc
{
"kind": 30078,
"content": "{\"name\":\"Amethyst\",\"identifier\":\"com.vitorpamplona.amethyst\",\"repository\":\"https://github.com/vitorpamplona/amethyst\",\"description\":\"...\"}",
"tags": [
["d", "app-com.vitorpamplona.amethyst"],
["t", "app-definition"],
["t", "nostr-clients"], // ← category tag
["name", "Amethyst"],
["repository", "https://github.com/vitorpamplona/amethyst"],
["gitea", "com.vitorpamplona.amethyst"],
["url", "https://cdn.zapstore.dev/..."],
["f", "android-arm64-v8a"],
["published_at", "1729302793"]
]
}
```
### App Review Events
Each model's review of an app is stored as a kind 30078 event with `#t: app-review`. The review is pinned to a specific version via SHA256 hash.
```jsonc
{
"kind": 30078,
"content": "{\"verdict\":\"APPROVED\",\"level\":1,\"summary\":\"...\",\"findings\":{...}}",
"tags": [
// Unique ID: model + app identifier + version
["d", "review-deepseek/deepseek-v4-flash-com.vitorpamplona.amethyst-v1.2.3"],
// Reference to the app definition event
["a", "30078:<pubkey>:app-com.vitorpamplona.amethyst", "wss://relay.zapstore.dev"],
// The SPECIFIC asset/APK that was reviewed (kind 3063 event)
["e", "<kind_3063_event_id>", "wss://relay.zapstore.dev"],
// Version info for querying
["version", "1.2.3"],
["version_code", "1234"],
// SHA256 hash of the reviewed APK — cryptographic pin
["x", "a1b2c3d4e5f6..."],
// Review metadata
["model", "deepseek/deepseek-v4-flash"],
["model_name", "DeepSeek Flash 4"],
["verdict", "APPROVED"],
["level", "1"],
["p", "<agent_pubkey>"],
["t", "app-review"]
]
}
```
### Tag Reference
| Tag | Event Type | Purpose |
|-----|-----------|---------|
| `d` | Both | Unique identifier (`app-{identifier}` or `review-{model}-{identifier}-v{version}`) |
| `t` | Both | `app-definition` or `app-review` — also carries category for app definitions |
| `name` | App definition | Human-readable app name |
| `repository` | App definition | Source code URL |
| `gitea` | App definition | Gitea mirror identifier |
| `url` | App definition | APK download URL |
| `f` | App definition | Platform (e.g. `android-arm64-v8a`) |
| `a` → kind 30078 | App review | Reference to the app definition event |
| `e` → kind 3063 | App review | The specific APK asset that was reviewed |
| `x` | App review | SHA256 hash — cryptographic pin of the reviewed binary |
| `version` / `version_code` | App review | Version info |
| `model` | App review | LLM model string (e.g. `deepseek/deepseek-v4-flash`) |
| `verdict` | App review | Review verdict |
| `level` | App review | Privacy level (1-4) |
| `p` | App review | Agent pubkey who performed the review |
### Page Load Flow
```
Page opens
→ Queries relay for kind 30078, #t: app-definition
→ Groups apps by #t category tags → builds categories
→ Queries relay for kind 30078, #t: app-review
→ Matches reviews to apps via a tags
→ Renders categories + apps + per-model review status
```
### Version Pinning Flow
```
App v1.2.3 defined (kind 30078, #t: app-definition)
└── APK asset (kind 3063, SHA256: abc...)
└── Review (kind 30078, #t: app-review, pinned via x tag)
App v1.2.4 published (kind 3063, SHA256: def...)
└── NEW review needed — old review's SHA256 doesn't match
```
If a bad actor modifies the app in v1.2.4, the review for v1.2.3 (with its SHA256 `abc...`) does not carry over. A new review must be performed on the new binary.
-149
View File
@@ -1,149 +0,0 @@
# App Stacks — Forward Plan
## Current State
[`www/app-stacks.html`](../www/app-stacks.html) has:
- A publish form that takes a Gitea URL + category and publishes a kind 30078 app-definition event
- A subscription that displays received app-definition events grouped by category
## New Understanding
- **Categories are actually app stacks** — kind 30267 events (App curation set)
- An **app stack** is a curated collection of apps, e.g. "Nostr Clients", "Bitcoin & Lightning Wallets"
- An **app definition** (kind 30078) belongs to an app stack, not a free-text category
- We need two separate publish flows:
1. **Create an App Stack** — publishes a kind 30267 event
2. **Publish App Definition** — selects an existing app stack to belong to
## Nostr Event Types
### App Stack (kind 30267)
```json
{
"kind": 30267,
"content": "{\"name\":\"Nostr Clients\",\"description\":\"Nostr-native social and communication clients.\"}",
"tags": [
["d", "nostr-clients"],
["t", "app-stack"],
["name", "Nostr Clients"],
["description", "Nostr-native social and communication clients."],
// References to apps in this stack:
["a", "30078:<pubkey>:app-com.vitorpamplona.amethyst", "wss://relay.zapstore.dev"],
["a", "30078:<pubkey>:app-com.example.other", "wss://relay.zapstore.dev"]
]
}
```
### App Definition (kind 30078) — updated
```json
{
"kind": 30078,
"content": "{\"name\":\"Amethyst\",\"identifier\":\"com.vitorpamplona.amethyst\",\"repository\":\"...\",\"description\":\"\"}",
"tags": [
["d", "app-com.vitorpamplona.amethyst"],
["t", "app-definition"],
["t", "nostr-clients"], // ← references the app stack's d tag
["name", "Amethyst"],
["repository", "https://github.com/vitorpamplona/amethyst"],
["gitea", "com.vitorpamplona.amethyst"]
]
}
```
## Implementation Phases
### Phase 1: App Stack CRUD
**Goal:** Create, list, and display app stacks (kind 30267 events).
**Changes to [`www/app-stacks.html`](../www/app-stacks.html) (JS only):**
1. **New subscription** — subscribe to kind 30267 with `#t: app-stack`:
```javascript
subscribe({ kinds: [30267], '#t': ['app-stack'], limit: 200 }, ...);
```
2. **New function: `parseAppStack(evt)`** — parse kind 30267 events into `{ dTag, name, description, appRefs: [], eventId }`
3. **New function: `renderStacks()`** — display app stacks in a section above or alongside app definitions
4. **New function: `publishAppStack(name, description)`** — create and publish a kind 30267 event:
```javascript
{
kind: 30267,
content: JSON.stringify({ name, description }),
tags: [
['d', name.toLowerCase().replace(/\s+/g, '-')],
['t', 'app-stack'],
['name', name],
['description', description],
],
created_at: Math.floor(Date.now() / 1000),
}
```
5. **New function: `showCreateStackForm()`** — form with name and description fields
6. **Update `renderApps()`** — add a "Create App Stack" button/area
### Phase 2: Link App Definitions to Stacks
**Goal:** App definitions select an app stack instead of typing a free-text category.
**Changes to [`www/app-stacks.html`](../www/app-stacks.html) (JS only):**
1. **Update publish form** — replace the free-text category input with a `<select>` dropdown populated from loaded app stacks
2. **Update `doPublish()`** — use the selected stack's `d` tag value as the `#t` category tag
3. **Update `renderApps()`** — group apps by the app stack they reference, not by free-text category
### Phase 3: Stack Membership Management
**Goal:** Add/remove apps from stacks, and display stack membership.
**Changes to [`www/app-stacks.html`](../www/app-stacks.html) (JS only):**
1. **Update `publishAppDefinition()`** — also add an `a` tag referencing the app stack:
```javascript
['a', '30267:<pubkey>:' + stackDTag, 'wss://relay.zapstore.dev']
```
2. **Display stack membership** — show which stack(s) each app belongs to
3. **Stack detail view** — show all apps in a stack
## Data Flow
```mermaid
flowchart TD
subgraph "Publishing"
A[Create App Stack form] --> B[publishEvent kind 30267]
C[Publish App Definition form] --> D[Select stack from dropdown]
D --> E[publishEvent kind 30078<br>with #t: stack-d-tag]
end
subgraph "Subscriptions"
F[Subscribe kind 30267 #t: app-stack] --> G[Collect stacks]
H[Subscribe kind 30078 #t: app-definition] --> I[Collect apps]
end
subgraph "Rendering"
G --> J[Render stack list]
I --> K[Group apps by stack reference]
K --> L[Render apps under each stack]
end
```
## Files to Modify
| File | Change |
|------|--------|
| [`www/app-stacks.html`](../www/app-stacks.html) | Add JS for kind 30267 subscription, parsing, rendering, publishing. Update app-definition form to use stack dropdown. |
| [`app-stacks/README.md`](README.md) | Update with new event structures and flow |
## What NOT to Change
- No HTML changes to the template shell
- No CSS additions
- No removal of existing functionality
-508
View File
@@ -1,508 +0,0 @@
#!/usr/bin/env python3
"""
Publish all 20 app stacks and all app definitions with Gitea mirror URLs.
Uses n_signer via Qubes qrexec (nostr_index=0) to sign events,
then publishes to all configured relays.
Usage:
python3 app-stacks/publish_all.py
"""
import asyncio
import json
import struct
import subprocess
import sys
import time
import websockets
QREXEC_TARGET = "nostr_signer"
QREXEC_SERVICE = "qubes.NsignerRpc"
RELAYS = [
"wss://relay.zapstore.dev",
"wss://laantungir.net/relay",
"wss://nos.lol",
"wss://relay.primal.net",
]
# ── All 20 stacks with their apps ──────────────────────────────────
STACKS = [
{
"d": "privacy-approved-bitcoin-lightning-wallets",
"name": "Privacy Approved — Bitcoin & Lightning Wallets",
"description": "Self-custodial Bitcoin and Lightning wallet apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("app.zeusln.zeus", "ZEUS"),
("io.nunchuk.android", "Nunchuk"),
("com.cakewallet.cake_wallet", "Cake Wallet"),
("com.getalby.mobile", "Alby Go"),
("com.blitzwallet", "Blitz Wallet"),
("io.bluewallet.bluewallet", "BlueWallet"),
("org.electrum.electrum", "Electrum"),
("com.oubli.wallet", "Oubli"),
("com.kilombino.mercasats", "MercaSats"),
("com.magius.lightningreaction", "Lightning Reaction"),
],
},
{
"d": "privacy-approved-nostr-clients",
"name": "Privacy Approved — Nostr Clients",
"description": "Nostr-native social and communication clients that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.vitorpamplona.amethyst", "Amethyst"),
("com.greenart7c3.nostrsigner", "Amber"),
("com.wisp.app", "Wisp"),
("com.darkwisp.app", "Dark Wisp"),
("org.parres.whitenoise", "White Noise"),
("com.nospeak.app", "Nospeak"),
("org.nostr.nostrord", "Nostrord"),
("com.shosho.app", "Shosho"),
("com.traveltelly.app", "TravelTelly"),
("app.nostrmail.client", "Nmail"),
("co.openvine.app", "Divine"),
("com.yakihonne.yakihonne", "YakiHonne"),
("com.nostrnests.app", "Nests"),
("app.nostria.twa", "Nostria"),
("dev.zapstore.alpha", "Zapstore Alpha"),
("com.pearcal", "PearCal"),
("com.pearcircle", "PearCircle"),
("pub.ditto.app", "Ditto"),
("social.flotilla", "Flotilla"),
],
},
{
"d": "privacy-approved-secure-messaging",
"name": "Privacy Approved — Secure Messaging",
"description": "End-to-end encrypted messaging and email apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("chat.simplex.app", "SimpleX"),
("eu.siacs.conversations", "Conversations"),
("chat.delta", "Delta Chat"),
("org.atalk.android", "aTalk"),
("im.vector.app", "Element"),
("io.element.android.x", "Element X"),
("net.thunderbird.android", "Thunderbird"),
("eu.faircode.email", "FairEmail"),
("de.tutao.tutanota", "Tuta"),
("com.quietmobile", "Quiet"),
("spam.blocker", "SpamBlocker"),
("com.afkanerd.deku", "Deku SMS"),
],
},
{
"d": "privacy-approved-vpn-privacy-tools",
"name": "Privacy Approved — VPN & Privacy Tools",
"description": "VPN clients, firewalls, and network privacy tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("org.torproject.android", "Orbot"),
("net.mullvad.mullvadvpn", "Mullvad VPN"),
("com.tailscale.ipn", "Tailscale"),
("org.amnezia.vpn", "AmneziaVPN"),
("ch.protonvpn.android", "Proton VPN"),
("com.celzero.bravedns", "Rethink"),
("com.emanuelef.remote_capture", "PCAPdroid"),
("com.zaneschepke.wireguardautotunnel", "WG Tunnel"),
("io.github.romanvht.byedpi", "ByeDPI"),
("xyz.malkki.neostumbler", "NeoStumbler"),
("eu.darken.sdmse", "SD Maid"),
("pan.alexander.tordnscrypt.stable", "InviZible Pro"),
("com.flashsphere.privatednsqs", "Private DNS Quick Setting"),
],
},
{
"d": "privacy-approved-password-managers-auth",
"name": "Privacy Approved — Password Managers & Auth",
"description": "Password managers, 2FA authenticators, and identity tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.x8bit.bitwarden", "Bitwarden"),
("com.kunzisoft.keepass.libre", "KeePassDX"),
("com.beemdevelopment.aegis", "Aegis"),
("pl.lebihan.authnkey", "Authnkey"),
("io.ente.auth.independent", "Ente Auth"),
("io.privkey.keep", "Keep"),
("net.aliasvault.app", "AliasVault"),
("com.jksalcedo.passvault", "PassVault"),
("com.jksalcedo.librefind", "LibreFind"),
],
},
{
"d": "privacy-approved-maps-navigation",
"name": "Privacy Approved — Maps & Navigation",
"description": "Offline maps, navigation, and location tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("app.organicmaps.web", "Organic Maps"),
("net.osmand.plus", "OsmAnd~"),
("de.westnordost.streetcomplete", "StreetComplete"),
("com.comaps.app", "CoMaps"),
("org.owntracks.android", "OwnTracks"),
("net.osmtracker", "OSMTracker"),
],
},
{
"d": "privacy-approved-media-players-streaming",
"name": "Privacy Approved — Media Players & Streaming",
"description": "Video and music players, streaming clients that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("org.videolan.vlc", "VLC"),
("org.schabi.newpipe", "NewPipe"),
("infinityloop1309.newpipeenhanced", "PipePipe"),
("org.oxycblt.auxio", "Auxio"),
("com.gokadzev.musify", "Musify"),
("com.metrolist.music", "Metrolist"),
("io.freetubeapp.freetube", "FreeTube"),
("app.zaptrax", "Zaptrax"),
("app.zappix", "Zappix"),
("app.marlboroadvance.mpvex", "mpvEx"),
("com.github.k1rakishou.chan", "KurobaEx"),
],
},
{
"d": "privacy-approved-productivity-notes",
"name": "Privacy Approved — Productivity & Notes",
"description": "Note-taking, task management, and productivity tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.flux", "Flux"),
("com.streetwriters.notesnook", "Notesnook"),
("io.github.quillpad", "Quillpad"),
("com.adilhanney.saber", "Saber"),
("at.bitfire.davdroid", "DAVx⁵"),
("com.superproductivity.superproductivity", "Super Productivity"),
("eu.fliegendurst.triliumdroid", "Trilium Notes"),
("ch.martinstoeckli.silentnotes", "SilentNotes"),
("com.dtonon.manent", "Manent"),
("jp.godzhigella.meiso", "Meiso"),
("com.nahnah.florid", "Florid"),
("com.plektos.app", "Plektos"),
("io.nurunuru.app", "Urn"),
("com.shub39.grit", "Grit"),
("com.electricdreams.numo", "Numo"),
("me.timeto.app", "timeto.me"),
("com.ferrarid.converterpro", "Converter NOW"),
],
},
{
"d": "privacy-approved-file-management-cloud-sync",
"name": "Privacy Approved — File Management & Cloud Sync",
"description": "Cloud storage, file sync, and file management tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.nextcloud.client", "Nextcloud"),
("com.github.catfriend1.syncthingfork", "Syncthing-Fork"),
("com.seafile.seadroid2", "Seafile"),
("com.example.file_transfer", "File transfer"),
("org.primftpd", "primitive ftpd"),
("de.astubenbord.paperless_mobile", "Paperless Mobile"),
("com.viscouspot.gitsync", "GitSync"),
("eu.heili.wormhole", "wormhole"),
("lu.knaff.alain.saf_sftp", "SFTP Documents Provider"),
("com.akylas.documentscanner", "OSS Document Scanner"),
],
},
{
"d": "privacy-approved-browsers",
"name": "Privacy Approved — Browsers",
"description": "Privacy-focused web browsers that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.duckduckgo.mobile.android", "DuckDuckGo"),
("com.cromite.app", "Cromite"),
],
},
{
"d": "privacy-approved-social-media",
"name": "Privacy Approved — Social Media",
"description": "Federated and alternative social media clients that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("org.joinmastodon.android", "Mastodon"),
("tw.nekomimi.nekogram", "Nekogram"),
("ml.docilealligator.infinityforreddit.plus", "Infinity+"),
("com.newsblur", "NewsBlur"),
("dev.msfjarvis.claw.android", "Claw"),
("com.streamatico.polymarketviewer", "Polymarket Viewer"),
],
},
{
"d": "privacy-approved-finance-budgeting",
"name": "Privacy Approved — Finance & Budgeting",
"description": "Personal finance, expense tracking, and budgeting apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.flux", "Flow"),
("com.pennywiseai.tracker", "Pennywise AI Tracker"),
("com.github.premnirmal.tickerwidget.dev", "Dev Stocks Widget"),
("com.pearguard", "PearGuard"),
("network.mostro.app", "Mostro"),
("eu.domob.shopt2", "Shopt"),
],
},
{
"d": "privacy-approved-health-fitness",
"name": "Privacy Approved — Health & Fitness",
"description": "Health tracking, diet, and fitness apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("org.nutritionfacts.dailydozen", "Daily Dozen"),
("com.antoniegil.astronia", "Astronia"),
],
},
{
"d": "privacy-approved-education-reference",
"name": "Privacy Approved — Education & Reference",
"description": "Learning, dictionary, and reference tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("org.freecodecamp", "freeCodeCamp"),
("de.felixnuesse.disky", "freeDictionary"),
("org.eu.mumulhl.ciyue", "Ciyue"),
("com.tavultesoft.kmapro", "Keyman"),
("com.hardbacknutter.nevertoomanybooks", "NeverTooManyBooks"),
("de.doen1el.calibrewebcompanion", "Calibre Web Companion"),
("io.github.adithya_jayan.myrepertoirapp.fdroid", "Repertoire"),
("org.listenbrainz.android", "ListenBrainz"),
("com.kgurgul.cpuinfo", "CPU Info"),
("com.mkulesh.micromath.plus", "microMathematics Plus"),
],
},
{
"d": "privacy-approved-photography-image-tools",
"name": "Privacy Approved — Photography & Image Tools",
"description": "Camera, photo editing, scanning, and gallery apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("tech.lolli.toolbox", "Image Toolbox"),
("org.fairscan.app", "FairScan"),
("ua.com.radiokot.photoprism", "PhotoPrism"),
("com.kjxbyz.picguard", "PicGuard"),
("com.dot.gallery", "Gallery apps"),
("com.deniscerri.ytdl", "YTDLnis"),
("me.elcaju", "ElCaju"),
],
},
{
"d": "privacy-approved-utilities-tools",
"name": "Privacy Approved — Utilities & Tools",
"description": "System utilities, converters, and general-purpose tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("de.markusfisch.android.binaryeye", "Binary Eye"),
("com.kgurgul.cpuinfo", "CPU Info"),
("com.ferrarid.converterpro", "Converter NOW"),
("me.hackerchick.catima", "Catima"),
("app.lawnchair.lawnicons", "Lawnicons"),
("app.simple.peri", "Peristyle"),
("org.exbin.bined.editor.android", "BinEd"),
("com.dergoogler.mmrl", "MMRL"),
("io.github.samolego.canta", "Canta"),
("deltazero.amarok.foss", "Amarok"),
("com.rama.mako", "Mako"),
("com.shub39.rush", "Rush"),
("helium314.keyboard", "HeliBoard"),
("com.best.deskclock", "DeskClock"),
("com.acszo.redomi", "Redomi"),
("com.arn.scrobble", "Scrobble"),
("com.nononsenseapps.feeder.play", "Feeder"),
("io.github.sms2email.sms2email", "SMS2Email"),
("app.simple.inure", "Inure"),
("com.unciv.app", "Unciv"),
],
},
{
"d": "privacy-approved-games",
"name": "Privacy Approved — Games",
"description": "Open source games across genres that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.shatteredpixel.shatteredpixeldungeon", "Shattered Pixel Dungeon"),
("org.wesnoth.wesnoth", "Wesnoth"),
("de.sesu8642.feudaltactics", "Feudal Tactics"),
("com.agateau.burgerparty", "Burger Party"),
("de.chadenas.cpudefense", "ChipDefense"),
("de.z11.roboyard", "Roboyard"),
("com.damasclash", "Damas Clash"),
("com.unciv.app", "Unciv"),
],
},
{
"d": "privacy-approved-communication-non-nostr",
"name": "Privacy Approved — Communication (Non-Nostr)",
"description": "Voice/video calls, remote desktop, and messaging apps that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("org.telegram.messenger.web", "Telegram"),
("com.carriez.flutter_hbb", "RustDesk"),
("io.unsigned.sideband", "Sideband"),
("com.geeksville.mesh", "Meshtastic"),
],
},
{
"d": "privacy-approved-development-tools",
"name": "Privacy Approved — Development Tools",
"description": "Code editors, Git clients, and developer tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("com.foxdebug.acode", "Acode"),
("com.viscouspot.gitsync", "GitSync"),
("org.exbin.bined.editor.android", "BinEd"),
("org.freecodecamp", "freeCodeCamp"),
("zed.rainxch.githubstore", "GitHub Store"),
("com.inspiredandroid.kai", "Kai 9000"),
],
},
{
"d": "privacy-approved-calendar-scheduling",
"name": "Privacy Approved — Calendar & Scheduling",
"description": "Calendar apps, scheduling, and time management tools that passed privacy review. No tracking SDKs, minimal permissions, open source.",
"apps": [
("app.formstr.calendar", "Calendar by Form*"),
("com.pearcal", "PearCal"),
("me.timeto.app", "timeto.me"),
("com.blankdev.sidestep", "Sidestep"),
],
},
]
# ── Gitea mirror URL lookup ────────────────────────────────────────
def gitea_url(identifier):
return f"https://laantungir.net/git/zapstore-mirror/{identifier}"
# ── n_signer qrexec helpers ────────────────────────────────────────
def qrexec_call(request):
framed = struct.pack(">I", len(json.dumps(request).encode())) + json.dumps(request).encode()
proc = subprocess.run(
["qrexec-client-vm", QREXEC_TARGET, QREXEC_SERVICE],
input=framed, capture_output=True, timeout=30
)
if proc.returncode != 0:
raise Exception(f"qrexec failed: {proc.stderr.decode()}")
resp_len = struct.unpack(">I", proc.stdout[:4])[0]
return json.loads(proc.stdout[4:4 + resp_len])
# ── Relay publish helper ───────────────────────────────────────────
async def publish_event(signed_event, label=""):
async def publish_one(relay):
try:
async with websockets.connect(relay, max_size=10_000_000, open_timeout=10) as ws:
await ws.send(json.dumps(["EVENT", signed_event]))
while True:
resp = await asyncio.wait_for(ws.recv(), timeout=10)
data = json.loads(resp)
if data[0] == "OK" and data[1] == signed_event["id"]:
return relay, data[2]
except Exception:
return relay, False
results = await asyncio.gather(*[publish_one(r) for r in RELAYS])
ok = sum(1 for _, a in results if a)
total = len(results)
print(f" {label}Published to {ok}/{total} relays")
return ok > 0
# ── Main ───────────────────────────────────────────────────────────
async def main():
print("=" * 60)
print("Publishing all app stacks and app definitions")
print("=" * 60)
# Get pubkey
print("\n1. Getting pubkey from n_signer...")
req = {"id": "1", "method": "nostr_get_public_key", "params": [{"nostr_index": 0}]}
resp = qrexec_call(req)
if "error" in resp:
print(f" Error: {resp['error']}")
sys.exit(1)
pubkey = resp["result"]
print(f" Pubkey: {pubkey}")
results = {"stacks": 0, "stacks_failed": 0, "apps": 0, "apps_failed": 0}
# ── Publish all stacks ──────────────────────────────────────────
print(f"\n2. Publishing {len(STACKS)} app stacks...")
for i, stack in enumerate(STACKS, 1):
created_at = int(time.time())
tags = [
["d", stack["d"]],
["t", "app-stack"],
["name", stack["name"]],
["description", stack["description"]],
["f", "android-arm64-v8a"],
["h", ""],
]
# Add a tag for each app (using kind 32267 references from old project)
# Since we don't have the exact pubkeys, we use placeholder references
for app_id, app_name in stack["apps"]:
tags.append(["a", f"32267:{pubkey}:{app_id}", "wss://relay.zapstore.dev"])
event = {
"kind": 30267,
"content": "",
"tags": tags,
"created_at": created_at,
"pubkey": pubkey,
}
req = {"id": "2", "method": "nostr_sign_event", "params": [json.dumps(event), {"nostr_index": 0}]}
resp = qrexec_call(req)
if "error" in resp:
print(f" [{i}/{len(STACKS)}] ❌ {stack['name']}: {resp['error']}")
results["stacks_failed"] += 1
continue
signed = json.loads(resp["result"])
print(f" [{i}/{len(STACKS)}] {stack['name']} ({len(stack['apps'])} apps) id={signed['id'][:16]}...")
await publish_event(signed, " ")
results["stacks"] += 1
# ── Publish all app definitions ─────────────────────────────────
total_apps = sum(len(s["apps"]) for s in STACKS)
print(f"\n3. Publishing {total_apps} app definitions...")
app_num = 0
for stack in STACKS:
for app_id, app_name in stack["apps"]:
app_num += 1
repo_url = gitea_url(app_id)
created_at = int(time.time())
event = {
"kind": 30078,
"content": json.dumps({
"name": app_name,
"identifier": app_id,
"repository": repo_url,
"description": "",
}),
"tags": [
["d", "app-" + app_id],
["t", "app-definition"],
["t", stack["d"]],
["name", app_name],
["repository", repo_url],
["gitea", app_id],
],
"created_at": created_at,
"pubkey": pubkey,
}
req = {"id": "3", "method": "nostr_sign_event", "params": [json.dumps(event), {"nostr_index": 0}]}
resp = qrexec_call(req)
if "error" in resp:
print(f" [{app_num}/{total_apps}] ❌ {app_name}: {resp['error']}")
results["apps_failed"] += 1
continue
signed = json.loads(resp["result"])
print(f" [{app_num}/{total_apps}] {app_name} ({app_id}) id={signed['id'][:16]}...")
await publish_event(signed, " ")
results["apps"] += 1
# ── Summary ─────────────────────────────────────────────────────
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f" Stacks: {results['stacks']} published, {results['stacks_failed']} failed")
print(f" Apps: {results['apps']} published, {results['apps_failed']} failed")
print(f" Total: {results['stacks'] + results['apps']} events")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env python3
"""
Publish a test App Stack (kind 30267) for "Nostr Clients" using n_signer via qrexec.
Usage:
python3 app-stacks/publish_test_stack.py
"""
import asyncio
import json
import struct
import subprocess
import sys
import time
import websockets
QREXEC_TARGET = "nostr_signer"
QREXEC_SERVICE = "qubes.NsignerRpc"
RELAY = "wss://relay.zapstore.dev"
def qrexec_call(request):
"""Send a framed JSON-RPC request via qrexec and return the response."""
framed = struct.pack(">I", len(json.dumps(request).encode())) + json.dumps(request).encode()
proc = subprocess.run(
["qrexec-client-vm", QREXEC_TARGET, QREXEC_SERVICE],
input=framed, capture_output=True, timeout=30
)
if proc.returncode != 0:
raise Exception(f"qrexec failed: {proc.stderr.decode()}")
resp_len = struct.unpack(">I", proc.stdout[:4])[0]
return json.loads(proc.stdout[4:4 + resp_len])
async def publish_event(signed_event):
"""Publish a signed event to the relay."""
async with websockets.connect(RELAY, max_size=10_000_000, open_timeout=10) as ws:
msg = json.dumps(["EVENT", signed_event])
await ws.send(msg)
while True:
try:
resp = await asyncio.wait_for(ws.recv(), timeout=10)
data = json.loads(resp)
if data[0] == "OK" and data[1] == signed_event["id"]:
accepted = data[2]
msg = data[3] if len(data) > 3 else ""
print(f" Relay: {'✅ ACCEPTED' if accepted else '❌ REJECTED'}: {msg}")
return accepted
except asyncio.TimeoutError:
print(" No OK response from relay")
return False
async def main():
print("=" * 60)
print("Publishing App Stack: Nostr Clients")
print("=" * 60)
# Step 1: Get pubkey for nostr_index 0
print("\n1. Getting pubkey from n_signer...")
req = {"id": "1", "method": "nostr_get_public_key", "params": [{"nostr_index": 0}]}
resp = qrexec_call(req)
if "error" in resp:
print(f" Error: {resp['error']}")
sys.exit(1)
pubkey = resp["result"]
print(f" Pubkey: {pubkey}")
# Step 2: Build the unsigned event
created_at = int(time.time())
event = {
"kind": 30267,
"content": "",
"tags": [
["d", "privacy-approved-nostr-clients"],
["t", "app-stack"],
["name", "Privacy Approved — Nostr Clients"],
["description", "Nostr-native social and communication clients that passed privacy review. No tracking SDKs, minimal permissions, open source."],
["f", "android-arm64-v8a"],
["h", ""],
["a", "32267:aa9047325603dacd4f8142093567973566de3b1e20a89557b728c3be4c6a844b:com.vitorpamplona.amethyst", "wss://relay.zapstore.dev"],
["a", "32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.greenart7c3.nostrsigner", "wss://relay.zapstore.dev"],
["a", "32267:e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb:com.wisp.app", "wss://relay.zapstore.dev"],
["a", "32267:e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb:com.darkwisp.app", "wss://relay.zapstore.dev"],
["a", "32267:75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7:org.parres.whitenoise", "wss://relay.zapstore.dev"],
["a", "32267:f55678aa1f5d554536d456b13beab04f636d63fdedd586fe38a4cb9ce48c90bc:com.nospeak.app", "wss://relay.zapstore.dev"],
["a", "32267:b2cdcb37d32533145c00c4f43d5e1e1deb7c67bceea7ef63f526ca4cab891633:org.nostr.nostrord", "wss://relay.zapstore.dev"],
["a", "32267:85df00a2f6a91845354c8d2d9fbab4002bb85b4225baeab60fafb2587c5038ea:com.shosho.app", "wss://relay.zapstore.dev"],
["a", "32267:7d33ba57d8a6e8869a1f1d5215254597594ac0dbfeb01b690def8c461b82db35:com.traveltelly.app", "wss://relay.zapstore.dev"],
["a", "32267:b22b06b051fd5232966a9344a634d956c3dc33a7f5ecdcad9ed11ddc4120a7f2:app.nostrmail.client", "wss://relay.zapstore.dev"],
["a", "32267:c4a39f1291291d452405cd8ddd798c4a29a3858c52cd0d843f1f6852cf17682e:co.openvine.app", "wss://relay.zapstore.dev"],
["a", "32267:20986fb83e775d96d188ca5c9df10ce6d613e0eb7e5768a0f0b12b37cdac21b3:com.yakihonne.yakihonne", "wss://relay.zapstore.dev"],
["a", "32267:3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24:com.nostrnests.app", "wss://relay.zapstore.dev"],
["a", "32267:d1bd33333733dcc411f0ee893b38b8522fc0de227fff459d99044ced9e65581b:app.nostria.twa", "wss://relay.zapstore.dev"],
["a", "32267:d1bd33333733dcc411f0ee893b38b8522fc0de227fff459d99044ced9e65581b:app.nostria", "wss://relay.zapstore.dev"],
["a", "32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:dev.zapstore.alpha", "wss://relay.zapstore.dev"],
["a", "32267:d70b722b67768cbf1e3ac777e2107bbc447a9768899b548bf694f71a2a6b52f8:com.pearcal", "wss://relay.zapstore.dev"],
["a", "32267:d70b722b67768cbf1e3ac777e2107bbc447a9768899b548bf694f71a2a6b52f8:com.pearcircle", "wss://relay.zapstore.dev"],
["a", "32267:781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5:pub.ditto.app", "wss://relay.zapstore.dev"],
["a", "32267:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:social.flotilla", "wss://relay.zapstore.dev"],
],
"created_at": created_at,
"pubkey": pubkey,
}
print(f"\n2. Signing event (kind 30267, d=privacy-approved-nostr-clients)...")
req = {
"id": "2",
"method": "nostr_sign_event",
"params": [json.dumps(event), {"nostr_index": 0}]
}
resp = qrexec_call(req)
if "error" in resp:
print(f" Error: {resp['error']}")
sys.exit(1)
signed_event = json.loads(resp["result"])
print(f" Event ID: {signed_event['id']}")
print(f" Signature: {signed_event['sig'][:20]}...")
# Step 3: Publish to relay
print(f"\n3. Publishing to {RELAY}...")
accepted = await publish_event(signed_event)
if accepted:
print(f"\n✅ Published successfully!")
print(f" Stack: privacy-approved-nostr-clients")
print(f" Apps: 19 Nostr clients")
else:
print(f"\n❌ Failed to publish")
# Save the signed event
with open("app-stacks/signed_stack_event.json", "w") as f:
json.dump(signed_event, f, indent=2)
print(f"\n Signed event saved to app-stacks/signed_stack_event.json")
if __name__ == "__main__":
asyncio.run(main())
-34
View File
@@ -1,34 +0,0 @@
{
"pubkey": "8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e",
"created_at": 1785796854,
"kind": 30078,
"tags": [
[
"d",
"app-app.zeusln.zeus"
],
[
"t",
"app-definition"
],
[
"t",
"privacy-approved-bitcoin-lightning-wallets"
],
[
"name",
"ZEUS"
],
[
"repository",
"https://laantungir.net/git/zapstore-mirror/app.zeusln.zeus"
],
[
"gitea",
"app.zeusln.zeus"
]
],
"content": "{\"name\": \"ZEUS\", \"identifier\": \"app.zeusln.zeus\", \"repository\": \"https://laantungir.net/git/zapstore-mirror/app.zeusln.zeus\", \"description\": \"\"}",
"id": "96a27857d9ce5799805596f42557c70f297030a52b82756f1c3a2331a6fe83fb",
"sig": "2888165c36f491213f0842a967d487fec350df40abe4a75d3bafa819fab0d635dfeed67c1db7d87a38ebd72118e2101c2121a34e8d89612490828faf02e66d6a"
}
-134
View File
@@ -1,134 +0,0 @@
{
"pubkey": "8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e",
"created_at": 1785796760,
"kind": 30267,
"tags": [
[
"d",
"privacy-approved-nostr-clients"
],
[
"t",
"app-stack"
],
[
"name",
"Privacy Approved \u2014 Nostr Clients"
],
[
"description",
"Nostr-native social and communication clients that passed privacy review. No tracking SDKs, minimal permissions, open source."
],
[
"f",
"android-arm64-v8a"
],
[
"h",
""
],
[
"a",
"32267:aa9047325603dacd4f8142093567973566de3b1e20a89557b728c3be4c6a844b:com.vitorpamplona.amethyst",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.greenart7c3.nostrsigner",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb:com.wisp.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb:com.darkwisp.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7:org.parres.whitenoise",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:f55678aa1f5d554536d456b13beab04f636d63fdedd586fe38a4cb9ce48c90bc:com.nospeak.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:b2cdcb37d32533145c00c4f43d5e1e1deb7c67bceea7ef63f526ca4cab891633:org.nostr.nostrord",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:85df00a2f6a91845354c8d2d9fbab4002bb85b4225baeab60fafb2587c5038ea:com.shosho.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:7d33ba57d8a6e8869a1f1d5215254597594ac0dbfeb01b690def8c461b82db35:com.traveltelly.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:b22b06b051fd5232966a9344a634d956c3dc33a7f5ecdcad9ed11ddc4120a7f2:app.nostrmail.client",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:c4a39f1291291d452405cd8ddd798c4a29a3858c52cd0d843f1f6852cf17682e:co.openvine.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:20986fb83e775d96d188ca5c9df10ce6d613e0eb7e5768a0f0b12b37cdac21b3:com.yakihonne.yakihonne",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24:com.nostrnests.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:d1bd33333733dcc411f0ee893b38b8522fc0de227fff459d99044ced9e65581b:app.nostria.twa",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:d1bd33333733dcc411f0ee893b38b8522fc0de227fff459d99044ced9e65581b:app.nostria",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:dev.zapstore.alpha",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:d70b722b67768cbf1e3ac777e2107bbc447a9768899b548bf694f71a2a6b52f8:com.pearcal",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:d70b722b67768cbf1e3ac777e2107bbc447a9768899b548bf694f71a2a6b52f8:com.pearcircle",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5:pub.ditto.app",
"wss://relay.zapstore.dev"
],
[
"a",
"32267:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:social.flotilla",
"wss://relay.zapstore.dev"
]
],
"content": "",
"id": "eaa7b4e87ca59322899b561ac159cdba788f4b5a36483812794908cda15bb6c9",
"sig": "d2aa1b64b1fe14a67cefdf665dafa06f13b2c96c63ebf5ec895660ce24b5bde67660e72f1ed8a68d9ecc2948a8c0274e69ecf67645f62793019d59e0bd7a6ee0"
}
-340
View File
@@ -1,340 +0,0 @@
# App Stacks and Their Apps
Source: [`zapstore_app_stacks`](file:///home/user/lt/zapstore_app_stacks) project.
Gitea mirror: `https://laantungir.net/git/zapstore-mirror/` (197 repos)
## 1. Bitcoin & Lightning Wallets
**d-tag:** `privacy-approved-bitcoin-lightning-wallets`
**Description:** Self-custodial Bitcoin and Lightning wallet apps that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| ZEUS | `app.zeusln.zeus` | [app.zeusln.zeus](https://laantungir.net/git/zapstore-mirror/app.zeusln.zeus) |
| Nunchuk | `io.nunchuk.android` | [io.nunchuk.android](https://laantungir.net/git/zapstore-mirror/io.nunchuk.android) |
| Cake Wallet | `com.cakewallet.cake_wallet` | [com.cakewallet.cake_wallet](https://laantungir.net/git/zapstore-mirror/com.cakewallet.cake_wallet) |
| Alby Go | `com.getalby.mobile` | [com.getalby.mobile](https://laantungir.net/git/zapstore-mirror/com.getalby.mobile) |
| Blitz Wallet | `com.blitzwallet` | [com.blitzwallet](https://laantungir.net/git/zapstore-mirror/com.blitzwallet) |
| BlueWallet | `io.bluewallet.bluewallet` | [io.bluewallet.bluewallet](https://laantungir.net/git/zapstore-mirror/io.bluewallet.bluewallet) |
| Electrum | `org.electrum.electrum` | [org.electrum.electrum](https://laantungir.net/git/zapstore-mirror/org.electrum.electrum) |
| Oubli | `com.oubli.wallet` | [com.oubli.wallet](https://laantungir.net/git/zapstore-mirror/com.oubli.wallet) |
| MercaSats | `com.kilombino.mercasats` | [com.kilombino.mercasats](https://laantungir.net/git/zapstore-mirror/com.kilombino.mercasats) |
| Lightning Reaction | `com.magius.lightningreaction` | [com.magius.lightningreaction](https://laantungir.net/git/zapstore-mirror/com.magius.lightningreaction) |
## 2. Nostr Clients
**d-tag:** `privacy-approved-nostr-clients`
**Description:** Nostr-native social and communication clients that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Amethyst | `com.vitorpamplona.amethyst` | [com.vitorpamplona.amethyst](https://laantungir.net/git/zapstore-mirror/com.vitorpamplona.amethyst) |
| Amber | `com.greenart7c3.nostrsigner` | [com.greenart7c3.nostrsigner](https://laantungir.net/git/zapstore-mirror/com.greenart7c3.nostrsigner) |
| Wisp | `com.wisp.app` | [com.wisp.app](https://laantungir.net/git/zapstore-mirror/com.wisp.app) |
| Dark Wisp | `com.darkwisp.app` | [com.darkwisp.app](https://laantungir.net/git/zapstore-mirror/com.darkwisp.app) |
| White Noise | `org.parres.whitenoise` | [org.parres.whitenoise](https://laantungir.net/git/zapstore-mirror/org.parres.whitenoise) |
| Nospeak | `com.nospeak.app` | [com.nospeak.app](https://laantungir.net/git/zapstore-mirror/com.nospeak.app) |
| Nostrord | `org.nostr.nostrord` | — |
| Shosho | `com.shosho.app` | [com.shosho.app](https://laantungir.net/git/zapstore-mirror/com.shosho.app) |
| TravelTelly | `com.traveltelly.app` | [com.traveltelly.app](https://laantungir.net/git/zapstore-mirror/com.traveltelly.app) |
| Nmail | `app.nostrmail.client` | [app.nostrmail.client](https://laantungir.net/git/zapstore-mirror/app.nostrmail.client) |
| Divine | `co.openvine.app` | [co.openvine.app](https://laantungir.net/git/zapstore-mirror/co.openvine.app) |
| YakiHonne | `com.yakihonne.yakihonne` | [com.yakihonne.yakihonne](https://laantungir.net/git/zapstore-mirror/com.yakihonne.yakihonne) |
| Nests | `com.nostrnests.app` | [com.nostrnests.app](https://laantungir.net/git/zapstore-mirror/com.nostrnests.app) |
| Nostria | `app.nostria.twa` | [app.nostria.twa](https://laantungir.net/git/zapstore-mirror/app.nostria.twa) |
| Zapstore Alpha | `dev.zapstore.alpha` | [dev.zapstore.alpha](https://laantungir.net/git/zapstore-mirror/dev.zapstore.alpha) |
| PearCal | `com.pearcal` | [com.pearcal](https://laantungir.net/git/zapstore-mirror/com.pearcal) |
| PearCircle | `com.pearcircle` | [com.pearcircle](https://laantungir.net/git/zapstore-mirror/com.pearcircle) |
| Ditto | `pub.ditto.app` | — |
| Flotilla | `com.flotilla` | — |
## 3. Secure Messaging
**d-tag:** `privacy-approved-secure-messaging`
**Description:** End-to-end encrypted messaging and email apps that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| SimpleX | `chat.simplex.app` | [chat.simplex.app](https://laantungir.net/git/zapstore-mirror/chat.simplex.app) |
| Conversations | `eu.siacs.conversations` | — |
| Delta Chat | `chat.delta` | [chat.delta](https://laantungir.net/git/zapstore-mirror/chat.delta) |
| aTalk | `org.atalk.android` | [org.atalk.android](https://laantungir.net/git/zapstore-mirror/org.atalk.android) |
| Element | `im.vector.app` | [im.vector.app](https://laantungir.net/git/zapstore-mirror/im.vector.app) |
| Element X | `io.element.android.x` | [io.element.android.x](https://laantungir.net/git/zapstore-mirror/io.element.android.x) |
| Thunderbird | `net.thunderbird.android` | [net.thunderbird.android](https://laantungir.net/git/zapstore-mirror/net.thunderbird.android) |
| FairEmail | `eu.faircode.email` | — |
| Tuta | `de.tutao.tutanota` | [de.tutao.tutanota](https://laantungir.net/git/zapstore-mirror/de.tutao.tutanota) |
| Quiet | `com.quietmobile` | [com.quietmobile](https://laantungir.net/git/zapstore-mirror/com.quietmobile) |
| SpamBlocker | `spam.blocker` | [spam.blocker](https://laantungir.net/git/zapstore-mirror/spam.blocker) |
| Deku SMS | `com.afkanerd.deku` | [com.afkanerd.deku](https://laantungir.net/git/zapstore-mirror/com.afkanerd.deku) |
## 4. VPN & Privacy Tools
**d-tag:** `privacy-approved-vpn-privacy-tools`
**Description:** VPN clients, firewalls, and network privacy tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Orbot | `org.torproject.android` | [org.torproject.android](https://laantungir.net/git/zapstore-mirror/org.torproject.android) |
| Mullvad VPN | `net.mullvad.mullvadvpn` | [net.mullvad.mullvadvpn](https://laantungir.net/git/zapstore-mirror/net.mullvad.mullvadvpn) |
| Tailscale | `com.tailscale.ipn` | [com.tailscale.ipn](https://laantungir.net/git/zapstore-mirror/com.tailscale.ipn) |
| AmneziaVPN | `org.amnezia.vpn` | [org.amnezia.vpn](https://laantungir.net/git/zapstore-mirror/org.amnezia.vpn) |
| Proton VPN | `ch.protonvpn.android` | [ch.protonvpn.android](https://laantungir.net/git/zapstore-mirror/ch.protonvpn.android) |
| Rethink | `com.celzero.bravedns` | [com.celzero.bravedns](https://laantungir.net/git/zapstore-mirror/com.celzero.bravedns) |
| PCAPdroid | `com.emanuelef.remote_capture` | [com.emanuelef.remote_capture](https://laantungir.net/git/zapstore-mirror/com.emanuelef.remote_capture) |
| WG Tunnel | `com.zaneschepke.wireguardautotunnel` | [com.zaneschepke.wireguardautotunnel](https://laantungir.net/git/zapstore-mirror/com.zaneschepke.wireguardautotunnel) |
| ByeDPI | `io.github.romanvht.byedpi` | [io.github.romanvht.byedpi](https://laantungir.net/git/zapstore-mirror/io.github.romanvht.byedpi) |
| NeoStumbler | `xyz.malkki.neostumbler` | [xyz.malkki.neostumbler](https://laantungir.net/git/zapstore-mirror/xyz.malkki.neostumbler) |
| SD Maid | `eu.darken.sdmse` | [eu.darken.sdmse](https://laantungir.net/git/zapstore-mirror/eu.darken.sdmse) |
| InviZible Pro | `pan.alexander.tordnscrypt.stable` | [pan.alexander.tordnscrypt.stable](https://laantungir.net/git/zapstore-mirror/pan.alexander.tordnscrypt.stable) |
| Private DNS Quick Setting | `com.flashsphere.privatednsqs` | [com.flashsphere.privatednsqs](https://laantungir.net/git/zapstore-mirror/com.flashsphere.privatednsqs) |
## 5. Password Managers & Auth
**d-tag:** `privacy-approved-password-managers-auth`
**Description:** Password managers, 2FA authenticators, and identity tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Bitwarden | `com.x8bit.bitwarden` | [com.x8bit.bitwarden](https://laantungir.net/git/zapstore-mirror/com.x8bit.bitwarden) |
| KeePassDX | `com.kunzisoft.keepass.libre` | [com.kunzisoft.keepass.libre](https://laantungir.net/git/zapstore-mirror/com.kunzisoft.keepass.libre) |
| Aegis | `com.beemdevelopment.aegis` | [com.beemdevelopment.aegis](https://laantungir.net/git/zapstore-mirror/com.beemdevelopment.aegis) |
| Authnkey | `pl.lebihan.authnkey` | [pl.lebihan.authnkey](https://laantungir.net/git/zapstore-mirror/pl.lebihan.authnkey) |
| Ente Auth | `io.ente.auth.independent` | [io.ente.auth.independent](https://laantungir.net/git/zapstore-mirror/io.ente.auth.independent) |
| Keep | `io.privkey.keep` | [io.privkey.keep](https://laantungir.net/git/zapstore-mirror/io.privkey.keep) |
| AliasVault | `net.aliasvault.app` | [net.aliasvault.app](https://laantungir.net/git/zapstore-mirror/net.aliasvault.app) |
| PassVault | `com.jksalcedo.passvault` | [com.jksalcedo.passvault](https://laantungir.net/git/zapstore-mirror/com.jksalcedo.passvault) |
| LibreFind | `com.jksalcedo.librefind` | [com.jksalcedo.librefind](https://laantungir.net/git/zapstore-mirror/com.jksalcedo.librefind) |
## 6. Maps & Navigation
**d-tag:** `privacy-approved-maps-navigation`
**Description:** Offline maps, navigation, and location tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Organic Maps | `app.organicmaps.web` | [app.organicmaps.web](https://laantungir.net/git/zapstore-mirror/app.organicmaps.web) |
| OsmAnd~ | `net.osmand.plus` | — |
| StreetComplete | `de.westnordost.streetcomplete` | [de.westnordost.streetcomplete](https://laantungir.net/git/zapstore-mirror/de.westnordost.streetcomplete) |
| CoMaps | `com.comaps.app` | — |
| OwnTracks | `org.owntracks.android` | [org.owntracks.android](https://laantungir.net/git/zapstore-mirror/org.owntracks.android) |
| OSMTracker | `net.osmtracker` | [net.osmtracker](https://laantungir.net/git/zapstore-mirror/net.osmtracker) |
## 7. Media Players & Streaming
**d-tag:** `privacy-approved-media-players-streaming`
**Description:** Video and music players, streaming clients that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| VLC | `org.videolan.vlc` | [org.videolan.vlc](https://laantungir.net/git/zapstore-mirror/org.videolan.vlc) |
| NewPipe | `org.schabi.newpipe` | [org.schabi.newpipe](https://laantungir.net/git/zapstore-mirror/org.schabi.newpipe) |
| PipePipe | `infinityloop1309.newpipeenhanced` | [infinityloop1309.newpipeenhanced](https://laantungir.net/git/zapstore-mirror/infinityloop1309.newpipeenhanced) |
| Auxio | `org.oxycblt.auxio` | [org.oxycblt.auxio](https://laantungir.net/git/zapstore-mirror/org.oxycblt.auxio) |
| Musify | `com.gokadzev.musify` | [com.gokadzev.musify](https://laantungir.net/git/zapstore-mirror/com.gokadzev.musify) |
| Metrolist | `com.metrolist.music` | [com.metrolist.music](https://laantungir.net/git/zapstore-mirror/com.metrolist.music) |
| FreeTube | `io.freetubeapp.freetube` | [io.freetubeapp.freetube](https://laantungir.net/git/zapstore-mirror/io.freetubeapp.freetube) |
| Zaptrax | `app.zaptrax` | [app.zaptrax](https://laantungir.net/git/zapstore-mirror/app.zaptrax) |
| Zappix | `app.zappix` | [app.zappix](https://laantungir.net/git/zapstore-mirror/app.zappix) |
| mpvEx | `app.marlboroadvance.mpvex` | [app.marlboroadvance.mpvex](https://laantungir.net/git/zapstore-mirror/app.marlboroadvance.mpvex) |
| KurobaEx | `com.github.k1rakishou.chan` | [com.github.k1rakishou.chan](https://laantungir.net/git/zapstore-mirror/com.github.k1rakishou.chan) |
## 8. Productivity & Notes
**d-tag:** `privacy-approved-productivity-notes`
**Description:** Note-taking, task management, and productivity tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Flux | `com.flux` | [com.flux](https://laantungir.net/git/zapstore-mirror/com.flux) |
| Notesnook | `com.streetwriters.notesnook` | [com.streetwriters.notesnook](https://laantungir.net/git/zapstore-mirror/com.streetwriters.notesnook) |
| Quillpad | `io.github.quillpad` | [io.github.quillpad](https://laantungir.net/git/zapstore-mirror/io.github.quillpad) |
| Saber | `com.adilhanney.saber` | [com.adilhanney.saber](https://laantungir.net/git/zapstore-mirror/com.adilhanney.saber) |
| DAVx⁵ | `at.bitfire.davdroid` | [at.bitfire.davdroid](https://laantungir.net/git/zapstore-mirror/at.bitfire.davdroid) |
| Super Productivity | `com.superproductivity.superproductivity` | [com.superproductivity.superproductivity](https://laantungir.net/git/zapstore-mirror/com.superproductivity.superproductivity) |
| Trilium Notes | `eu.fliegendurst.triliumdroid` | — |
| SilentNotes | `ch.martinstoeckli.silentnotes` | [ch.martinstoeckli.silentnotes](https://laantungir.net/git/zapstore-mirror/ch.martinstoeckli.silentnotes) |
| Manent | `com.dtonon.manent` | [com.dtonon.manent](https://laantungir.net/git/zapstore-mirror/com.dtonon.manent) |
| Meiso | `jp.godzhigella.meiso` | [jp.godzhigella.meiso](https://laantungir.net/git/zapstore-mirror/jp.godzhigella.meiso) |
| Florid | `com.nahnah.florid` | [com.nahnah.florid](https://laantungir.net/git/zapstore-mirror/com.nahnah.florid) |
| Plektos | `com.plektos.app` | [com.plektos.app](https://laantungir.net/git/zapstore-mirror/com.plektos.app) |
| Urn | `io.nurunuru.app` | [io.nurunuru.app](https://laantungir.net/git/zapstore-mirror/io.nurunuru.app) |
| Screen Time | `com.screentime.app` | — |
| Grit | `com.shub39.grit` | [com.shub39.grit](https://laantungir.net/git/zapstore-mirror/com.shub39.grit) |
| Numo | `com.electricdreams.numo` | [com.electricdreams.numo](https://laantungir.net/git/zapstore-mirror/com.electricdreams.numo) |
| timeto.me | `me.timeto.app` | [me.timeto.app](https://laantungir.net/git/zapstore-mirror/me.timeto.app) |
| Converter NOW | `com.ferrarid.converterpro` | [com.ferrarid.converterpro](https://laantungir.net/git/zapstore-mirror/com.ferrarid.converterpro) |
## 9. File Management & Cloud Sync
**d-tag:** `privacy-approved-file-management-cloud-sync`
**Description:** Cloud storage, file sync, and file management tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Nextcloud | `com.nextcloud.client` | [com.nextcloud.client](https://laantungir.net/git/zapstore-mirror/com.nextcloud.client) |
| Syncthing-Fork | `com.github.catfriend1.syncthingfork` | [com.github.catfriend1.syncthingfork](https://laantungir.net/git/zapstore-mirror/com.github.catfriend1.syncthingfork) |
| Seafile | `com.seafile.seadroid2` | [com.seafile.seadroid2](https://laantungir.net/git/zapstore-mirror/com.seafile.seadroid2) |
| File transfer | `com.example.file_transfer` | [com.example.file_transfer](https://laantungir.net/git/zapstore-mirror/com.example.file_transfer) |
| primitive ftpd | `org.primftpd` | [org.primftpd](https://laantungir.net/git/zapstore-mirror/org.primftpd) |
| Paperless Mobile | `de.astubenbord.paperless_mobile` | [de.astubenbord.paperless_mobile](https://laantungir.net/git/zapstore-mirror/de.astubenbord.paperless_mobile) |
| GitSync | `com.viscouspot.gitsync` | [com.viscouspot.gitsync](https://laantungir.net/git/zapstore-mirror/com.viscouspot.gitsync) |
| wormhole | `eu.heili.wormhole` | [eu.heili.wormhole](https://laantungir.net/git/zapstore-mirror/eu.heili.wormhole) |
| SFTP Documents Provider | `lu.knaff.alain.saf_sftp` | [lu.knaff.alain.saf_sftp](https://laantungir.net/git/zapstore-mirror/lu.knaff.alain.saf_sftp) |
| OSS Document Scanner | `com.akylas.documentscanner` | [com.akylas.documentscanner](https://laantungir.net/git/zapstore-mirror/com.akylas.documentscanner) |
## 10. Browsers
**d-tag:** `privacy-approved-browsers`
**Description:** Privacy-focused web browsers that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| DuckDuckGo | `com.duckduckgo.mobile.android` | — |
| Cromite | `com.cromite.app` | — |
## 11. Social Media
**d-tag:** `privacy-approved-social-media`
**Description:** Federated and alternative social media clients that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Mastodon | `org.joinmastodon.android` | [org.joinmastodon.android](https://laantungir.net/git/zapstore-mirror/org.joinmastodon.android) |
| Nekogram | `tw.nekomimi.nekogram` | [tw.nekomimi.nekogram](https://laantungir.net/git/zapstore-mirror/tw.nekomimi.nekogram) |
| Infinity+ | `ml.docilealligator.infinityforreddit.plus` | [ml.docilealligator.infinityforreddit.plus](https://laantungir.net/git/zapstore-mirror/ml.docilealligator.infinityforreddit.plus) |
| NewsBlur | `com.newsblur` | [com.newsblur](https://laantungir.net/git/zapstore-mirror/com.newsblur) |
| Claw | `dev.msfjarvis.claw.android` | [dev.msfjarvis.claw.android](https://laantungir.net/git/zapstore-mirror/dev.msfjarvis.claw.android) |
| Polymarket Viewer | `com.streamatico.polymarketviewer` | [com.streamatico.polymarketviewer](https://laantungir.net/git/zapstore-mirror/com.streamatico.polymarketviewer) |
## 12. Finance & Budgeting
**d-tag:** `privacy-approved-finance-budgeting`
**Description:** Personal finance, expense tracking, and budgeting apps that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Flow | `com.flow` | — |
| Pennywise AI Tracker | `com.pennywiseai.tracker` | [com.pennywiseai.tracker](https://laantungir.net/git/zapstore-mirror/com.pennywiseai.tracker) |
| Dev Stocks Widget | `com.github.premnirmal.tickerwidget.dev` | [com.github.premnirmal.tickerwidget.dev](https://laantungir.net/git/zapstore-mirror/com.github.premnirmal.tickerwidget.dev) |
| Seeker | `com.seeker.app` | — |
| PearGuard | `com.pearguard` | [com.pearguard](https://laantungir.net/git/zapstore-mirror/com.pearguard) |
| Mostro | `network.mostro.app` | [network.mostro.app](https://laantungir.net/git/zapstore-mirror/network.mostro.app) |
| Shopt | `eu.domob.shopt2` | [eu.domob.shopt2](https://laantungir.net/git/zapstore-mirror/eu.domob.shopt2) |
## 13. Health & Fitness
**d-tag:** `privacy-approved-health-fitness`
**Description:** Health tracking, diet, and fitness apps that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Daily Dozen | `org.nutritionfacts.dailydozen` | [org.nutritionfacts.dailydozen](https://laantungir.net/git/zapstore-mirror/org.nutritionfacts.dailydozen) |
| Screen Time | `com.screentime.app` | — |
| Astronia | `com.antoniegil.astronia` | [com.antoniegil.astronia](https://laantungir.net/git/zapstore-mirror/com.antoniegil.astronia) |
## 14. Education & Reference
**d-tag:** `privacy-approved-education-reference`
**Description:** Learning, dictionary, and reference tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| freeCodeCamp | `org.freecodecamp` | [org.freecodecamp](https://laantungir.net/git/zapstore-mirror/org.freecodecamp) |
| freeDictionary | `de.felixnuesse.disky` | [de.felixnuesse.disky](https://laantungir.net/git/zapstore-mirror/de.felixnuesse.disky) |
| Ciyue | `org.eu.mumulhl.ciyue` | [org.eu.mumulhl.ciyue](https://laantungir.net/git/zapstore-mirror/org.eu.mumulhl.ciyue) |
| Keyman | `com.tavultesoft.kmapro` | [com.tavultesoft.kmapro](https://laantungir.net/git/zapstore-mirror/com.tavultesoft.kmapro) |
| NeverTooManyBooks | `com.hardbacknutter.nevertoomanybooks` | [com.hardbacknutter.nevertoomanybooks](https://laantungir.net/git/zapstore-mirror/com.hardbacknutter.nevertoomanybooks) |
| Calibre Web Companion | `de.doen1el.calibrewebcompanion` | [de.doen1el.calibrewebcompanion](https://laantungir.net/git/zapstore-mirror/de.doen1el.calibrewebcompanion) |
| Repertoire | `io.github.adithya_jayan.myrepertoirapp.fdroid` | [io.github.adithya_jayan.myrepertoirapp.fdroid](https://laantungir.net/git/zapstore-mirror/io.github.adithya_jayan.myrepertoirapp.fdroid) |
| ListenBrainz | `org.listenbrainz.android` | [org.listenbrainz.android](https://laantungir.net/git/zapstore-mirror/org.listenbrainz.android) |
| CPU Info | `com.kgurgul.cpuinfo` | [com.kgurgul.cpuinfo](https://laantungir.net/git/zapstore-mirror/com.kgurgul.cpuinfo) |
| microMathematics Plus | `com.mkulesh.micromath.plus` | [com.mkulesh.micromath.plus](https://laantungir.net/git/zapstore-mirror/com.mkulesh.micromath.plus) |
| Mental Math | `com.mental.math` | — |
## 15. Photography & Image Tools
**d-tag:** `privacy-approved-photography-image-tools`
**Description:** Camera, photo editing, scanning, and gallery apps that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Image Toolbox | `tech.lolli.toolbox` | [tech.lolli.toolbox](https://laantungir.net/git/zapstore-mirror/tech.lolli.toolbox) |
| FairScan | `org.fairscan.app` | [org.fairscan.app](https://laantungir.net/git/zapstore-mirror/org.fairscan.app) |
| PhotoPrism | `ua.com.radiokot.photoprism` | [ua.com.radiokot.photoprism](https://laantungir.net/git/zapstore-mirror/ua.com.radiokot.photoprism) |
| PicGuard | `com.kjxbyz.picguard` | [com.kjxbyz.picguard](https://laantungir.net/git/zapstore-mirror/com.kjxbyz.picguard) |
| Gallery apps | `com.dot.gallery` | [com.dot.gallery](https://laantungir.net/git/zapstore-mirror/com.dot.gallery) |
| YTDLnis | `com.deniscerri.ytdl` | [com.deniscerri.ytdl](https://laantungir.net/git/zapstore-mirror/com.deniscerri.ytdl) |
| ElCaju | `me.elcaju` | [me.elcaju](https://laantungir.net/git/zapstore-mirror/me.elcaju) |
| Espy | `com.espy.app` | — |
| PDF Wallet | `com.pdf.wallet` | — |
## 16. Utilities & Tools
**d-tag:** `privacy-approved-utilities-tools`
**Description:** System utilities, converters, and general-purpose tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Binary Eye | `de.markusfisch.android.binaryeye` | [de.markusfisch.android.binaryeye](https://laantungir.net/git/zapstore-mirror/de.markusfisch.android.binaryeye) |
| CPU Info | `com.kgurgul.cpuinfo` | [com.kgurgul.cpuinfo](https://laantungir.net/git/zapstore-mirror/com.kgurgul.cpuinfo) |
| Converter NOW | `com.ferrarid.converterpro` | [com.ferrarid.converterpro](https://laantungir.net/git/zapstore-mirror/com.ferrarid.converterpro) |
| Catima | `me.hackerchick.catima` | [me.hackerchick.catima](https://laantungir.net/git/zapstore-mirror/me.hackerchick.catima) |
| BT Remote | `com.bt.remote` | — |
| Lawnicons | `app.lawnchair.lawnicons` | [app.lawnchair.lawnicons](https://laantungir.net/git/zapstore-mirror/app.lawnchair.lawnicons) |
| Peristyle | `app.simple.peri` | [app.simple.peri](https://laantungir.net/git/zapstore-mirror/app.simple.peri) |
| BinEd | `org.exbin.bined.editor.android` | [org.exbin.bined.editor.android](https://laantungir.net/git/zapstore-mirror/org.exbin.bined.editor.android) |
| MMRL | `com.dergoogler.mmrl` | [com.dergoogler.mmrl](https://laantungir.net/git/zapstore-mirror/com.dergoogler.mmrl) |
| Canta | `io.github.samolego.canta` | [io.github.samolego.canta](https://laantungir.net/git/zapstore-mirror/io.github.samolego.canta) |
| Amarok | `deltazero.amarok.foss` | [deltazero.amarok.foss](https://laantungir.net/git/zapstore-mirror/deltazero.amarok.foss) |
| ServerBox | `com.serverbox.app` | — |
| wX | `com.wx.app` | — |
| Mako | `com.rama.mako` | [com.rama.mako](https://laantungir.net/git/zapstore-mirror/com.rama.mako) |
| Rush | `com.shub39.rush` | [com.shub39.rush](https://laantungir.net/git/zapstore-mirror/com.shub39.rush) |
| HeliBoard | `helium314.keyboard` | [helium314.keyboard](https://laantungir.net/git/zapstore-mirror/helium314.keyboard) |
| DeskClock | `com.best.deskclock` | [com.best.deskclock](https://laantungir.net/git/zapstore-mirror/com.best.deskclock) |
| Redomi | `com.acszo.redomi` | [com.acszo.redomi](https://laantungir.net/git/zapstore-mirror/com.acszo.redomi) |
| Scrobble | `com.arn.scrobble` | [com.arn.scrobble](https://laantungir.net/git/zapstore-mirror/com.arn.scrobble) |
| Feeder | `com.nononsenseapps.feeder.play` | [com.nononsenseapps.feeder.play](https://laantungir.net/git/zapstore-mirror/com.nononsenseapps.feeder.play) |
| SMS2Email | `io.github.sms2email.sms2email` | [io.github.sms2email.sms2email](https://laantungir.net/git/zapstore-mirror/io.github.sms2email.sms2email) |
| Inure | `app.simple.inure` | [app.simple.inure](https://laantungir.net/git/zapstore-mirror/app.simple.inure) |
| Unciv | `com.unciv.app` | [com.unciv.app](https://laantungir.net/git/zapstore-mirror/com.unciv.app) |
## 17. Games
**d-tag:** `privacy-approved-games`
**Description:** Open source games across genres that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Shattered Pixel Dungeon | `com.shatteredpixel.shatteredpixeldungeon` | [com.shatteredpixel.shatteredpixeldungeon](https://laantungir.net/git/zapstore-mirror/com.shatteredpixel.shatteredpixeldungeon) |
| Wesnoth | `org.wesnoth.wesnoth` | [org.wesnoth.wesnoth](https://laantungir.net/git/zapstore-mirror/org.wesnoth.wesnoth) |
| Feudal Tactics | `de.sesu8642.feudaltactics` | [de.sesu8642.feudaltactics](https://laantungir.net/git/zapstore-mirror/de.sesu8642.feudaltactics) |
| Burger Party | `com.agateau.burgerparty` | [com.agateau.burgerparty](https://laantungir.net/git/zapstore-mirror/com.agateau.burgerparty) |
| ChipDefense | `de.chadenas.cpudefense` | [de.chadenas.cpudefense](https://laantungir.net/git/zapstore-mirror/de.chadenas.cpudefense) |
| Roboyard | `de.z11.roboyard` | [de.z11.roboyard](https://laantungir.net/git/zapstore-mirror/de.z11.roboyard) |
| Breakout 71 | `com.breakout71` | — |
| Damas Clash | `com.damasclash` | [com.damasclash](https://laantungir.net/git/zapstore-mirror/com.damasclash) |
| Unciv | `com.unciv.app` | [com.unciv.app](https://laantungir.net/git/zapstore-mirror/com.unciv.app) |
## 18. Communication (Non-Nostr)
**d-tag:** `privacy-approved-communication-non-nostr`
**Description:** Voice/video calls, remote desktop, and messaging apps that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Telegram | `org.telegram.messenger.web` | [org.telegram.messenger.web](https://laantungir.net/git/zapstore-mirror/org.telegram.messenger.web) |
| RustDesk | `com.carriez.flutter_hbb` | [com.carriez.flutter_hbb](https://laantungir.net/git/zapstore-mirror/com.carriez.flutter_hbb) |
| Sideband | `io.unsigned.sideband` | [io.unsigned.sideband](https://laantungir.net/git/zapstore-mirror/io.unsigned.sideband) |
| Meshtastic | `com.geeksville.mesh` | [com.geeksville.mesh](https://laantungir.net/git/zapstore-mirror/com.geeksville.mesh) |
## 19. Development Tools
**d-tag:** `privacy-approved-development-tools`
**Description:** Code editors, Git clients, and developer tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Acode | `com.foxdebug.acode` | [com.foxdebug.acode](https://laantungir.net/git/zapstore-mirror/com.foxdebug.acode) |
| GitSync | `com.viscouspot.gitsync` | [com.viscouspot.gitsync](https://laantungir.net/git/zapstore-mirror/com.viscouspot.gitsync) |
| BinEd | `org.exbin.bined.editor.android` | [org.exbin.bined.editor.android](https://laantungir.net/git/zapstore-mirror/org.exbin.bined.editor.android) |
| freeCodeCamp | `org.freecodecamp` | [org.freecodecamp](https://laantungir.net/git/zapstore-mirror/org.freecodecamp) |
| GitHub Store | `zed.rainxch.githubstore` | [zed.rainxch.githubstore](https://laantungir.net/git/zapstore-mirror/zed.rainxch.githubstore) |
| Kai 9000 | `com.inspiredandroid.kai` | [com.inspiredandroid.kai](https://laantungir.net/git/zapstore-mirror/com.inspiredandroid.kai) |
## 20. Calendar & Scheduling
**d-tag:** `privacy-approved-calendar-scheduling`
**Description:** Calendar apps, scheduling, and time management tools that passed privacy review.
| App | Identifier | Gitea URL |
|-----|-----------|-----------|
| Calendar by Form* | `app.formstr.calendar` | [app.formstr.calendar](https://laantungir.net/git/zapstore-mirror/app.formstr.calendar) |
| PearCal | `com.pearcal` | [com.pearcal](https://laantungir.net/git/zapstore-mirror/com.pearcal) |
| timeto.me | `me.timeto.app` | [me.timeto.app](https://laantungir.net/git/zapstore-mirror/me.timeto.app) |
| Sidestep | `com.blankdev.sidestep` | [com.blankdev.sidestep](https://laantungir.net/git/zapstore-mirror/com.blankdev.sidestep) |
---
**Total:** 20 stacks, 192 apps mapped.
+224 -59
View File
@@ -201,6 +201,13 @@
let stacks = [];
let stackSub = null;
let stacksLoaded = false;
// App versions state (kind 3063 Software Asset events)
let appVersions = {}; // identifier -> { version, size, url, created_at }
let assetSub = null;
let assetsLoaded = false;
let assetEventCount = 0; // total kind 3063 events received
let assetOldestTimestamp = Infinity; // oldest raw event timestamp seen (for pagination)
/*
AUTH STATE MODEL (Template reference)
@@ -570,34 +577,12 @@
'<div><button class="btn" onclick="doCreateStack()">Create Stack</button></div>' +
'</div>';
// === STACKS SECTION ===
var stacksHtml = '<div style="margin-bottom:15px;">' +
'<div style="font-weight:bold;font-size:14px;margin-bottom:8px;">App Stacks <span style="font-size:12px;color:var(--muted-color);">(' + stacks.length + ')</span></div>';
if (stacks.length === 0) {
stacksHtml += '<div style="font-size:12px;color:var(--muted-color);margin-bottom:6px;">No stacks yet.</div>';
} else {
stacks.forEach(function(s) {
stacksHtml +=
'<div style="border:1px solid var(--border-color);border-radius:var(--border-radius);padding:8px;margin-bottom:6px;font-size:12px;">' +
'<div style="font-weight:bold;">' + esc(s.name) + '</div>' +
'<div style="color:var(--muted-color);">d: ' + esc(s.dTag) + '</div>' +
(s.description ? '<div style="color:var(--muted-color);">' + esc(s.description) + '</div>' : '') +
'<div style="color:var(--muted-color);font-size:11px;">' + s.appRefs.length + ' app(s) · by ' + s.pubkey.substring(0, 8) + '&hellip;' + s.pubkey.substring(60) + '</div>' +
'</div>';
});
}
stacksHtml += '</div>';
// === APP DEFINITIONS SECTION ===
// Collect unique categories from loaded apps
var categories = {};
apps.forEach(function(a) { if (a.category) categories[a.category] = true; });
var categoryList = Object.keys(categories).sort();
var datalistOptions = categoryList.map(function(c) {
return '<option value="' + esc(c) + '">';
// === PUBLISH APP DEFINITION FORM ===
// Collect unique stack d-tags from loaded stacks for the dropdown
var stackOptions = stacks.map(function(s) {
return '<option value="' + esc(s.dTag) + '">' + esc(s.name) + '</option>';
}).join('');
// Publish form with datalist for category
var formHtml =
'<div style="max-width:500px;margin-bottom:15px;border:1px solid var(--border-color);border-radius:var(--border-radius);padding:12px;">' +
'<div style="font-weight:bold;margin-bottom:10px;">Publish App Definition</div>' +
@@ -605,50 +590,128 @@
'<input id="pubRepoUrl" type="text" placeholder="Gitea Repo URL (e.g. https://git.laantungir.net/owner/repo)" style="width:100%;padding:8px;font-family:var(--font-family);font-size:13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--secondary-color);color:var(--color);">' +
'</div>' +
'<div style="margin-bottom:8px;">' +
'<input id="pubCategory" list="pubCategoryList" type="text" placeholder="Category (e.g. nostr-clients, wallets)" style="width:100%;padding:8px;font-family:var(--font-family);font-size:13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--secondary-color);color:var(--color);">' +
'<datalist id="pubCategoryList">' + datalistOptions + '</datalist>' +
'<select id="pubStack" style="width:100%;padding:8px;font-family:var(--font-family);font-size:13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--secondary-color);color:var(--color);">' +
'<option value="">Select an app stack...</option>' + stackOptions +
'</select>' +
'</div>' +
'<div><button class="btn" onclick="doPublish()">Publish</button></div>' +
'</div>';
// Group apps by category
var grouped = {};
// === STACKS WITH APPS ===
// Build a map of stack d-tag -> apps
var appsByStack = {};
apps.forEach(function(a) {
var cat = a.category || 'uncategorized';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(a);
if (!appsByStack[cat]) appsByStack[cat] = [];
appsByStack[cat].push(a);
});
var catOrder = Object.keys(grouped).sort();
var listHtml;
if (apps.length === 0) {
listHtml = '<div style="text-align:center;padding:20px;color:var(--muted-color);">No app definitions found yet.</div>';
var contentHtml = '';
if (stacks.length === 0 && apps.length === 0) {
contentHtml = '<div style="text-align:center;padding:20px;color:var(--muted-color);">No stacks or apps yet. Create a stack above, then publish apps into it.</div>';
} else {
listHtml = '<div style="margin-bottom:10px;font-size:13px;">' + apps.length + ' app' + (apps.length !== 1 ? 's' : '') + '</div>';
catOrder.forEach(function(cat) {
var catApps = grouped[cat];
listHtml +=
'<div style="margin-bottom:12px;">' +
'<div style="font-weight:bold;font-size:13px;margin-bottom:6px;border-bottom:1px solid var(--border-color);padding-bottom:3px;">' + esc(cat) + ' (' + catApps.length + ')</div>';
catApps.forEach(function(a) {
listHtml +=
'<div style="border:1px solid var(--border-color);border-radius:var(--border-radius);padding:10px;margin-bottom:6px;font-size:12px;">' +
'<div style="font-weight:bold;font-size:13px;margin-bottom:4px;">' + esc(a.name) + '</div>' +
'<div><span style="color:var(--muted-color);">identifier:</span> ' + esc(a.identifier) + '</div>' +
'<div><span style="color:var(--muted-color);">pubkey:</span> ' + esc(a.pubkey) + '</div>' +
'<div><span style="color:var(--muted-color);">repository:</span> ' + (a.repository ? '<a href="' + esc(a.repository) + '" target="_blank" rel="noopener">' + esc(a.repository) + '</a>' : '—') + '</div>' +
'<div><span style="color:var(--muted-color);">description:</span> ' + esc(a.description || '—') + '</div>' +
'<div><span style="color:var(--muted-color);">eventId:</span> ' + esc(a.eventId) + '</div>' +
// Show stacks that have apps, then stacks without apps, then apps without stacks
var stacksWithApps = [];
var stacksWithoutApps = [];
stacks.forEach(function(s) {
if (appsByStack[s.dTag] && appsByStack[s.dTag].length > 0) {
stacksWithApps.push(s);
} else {
stacksWithoutApps.push(s);
}
});
// Render stacks with apps
stacksWithApps.forEach(function(s) {
var stackApps = appsByStack[s.dTag] || [];
contentHtml +=
'<div style="border:1px solid var(--border-color);border-radius:var(--border-radius);margin-bottom:10px;">' +
'<div style="font-weight:bold;font-size:14px;padding:10px 12px;border-bottom:1px solid var(--border-color);cursor:pointer;" onclick="toggleStackApps(this)">' +
esc(s.name) + ' <span style="font-size:12px;color:var(--muted-color);">(' + stackApps.length + ')</span>' +
' <span style="font-size:11px;color:var(--muted-color);">▼</span>' +
'</div>' +
'<div class="stackAppsBody">';
stackApps.forEach(function(a) {
contentHtml +=
'<div style="border-bottom:1px solid var(--border-color);font-size:12px;">' +
'<div style="padding:8px 12px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;" onclick="toggleAppDetail(this)">' +
'<span><span style="font-weight:bold;">' + esc(a.name) + '</span> <span style="color:var(--muted-color);">' + esc(appVersions[a.identifier] ? 'v' + appVersions[a.identifier].version : a.identifier) + '</span></span>' +
'<span style="font-size:11px;color:var(--muted-color);">▶</span>' +
'</div>' +
'<div class="appDetailBody" style="display:none;padding:0 12px 8px 12px;font-size:12px;color:var(--muted-color);">' +
'<div><span style="color:var(--color);">identifier:</span> ' + esc(a.identifier) + '</div>' +
'<div><span style="color:var(--color);">version:</span> ' + esc(appVersions[a.identifier] ? appVersions[a.identifier].version : '—') + '</div>' +
'<div><span style="color:var(--color);">pubkey:</span> ' + esc(a.pubkey) + '</div>' +
'<div><span style="color:var(--color);">repository:</span> ' + (a.repository ? '<a href="' + esc(a.repository) + '" target="_blank" rel="noopener">' + esc(a.repository) + '</a>' : '—') + '</div>' +
'<div><span style="color:var(--color);">description:</span> ' + esc(a.description || '—') + '</div>' +
'<div><span style="color:var(--color);">eventId:</span> ' + esc(a.eventId) + '</div>' +
'</div>' +
'</div>';
});
listHtml += '</div>';
contentHtml += '</div></div>';
});
// Render stacks without apps
stacksWithoutApps.forEach(function(s) {
contentHtml +=
'<div style="border:1px solid var(--border-color);border-radius:var(--border-radius);padding:10px 12px;margin-bottom:6px;font-size:12px;">' +
'<div style="font-weight:bold;">' + esc(s.name) + '</div>' +
'<div style="color:var(--muted-color);font-size:11px;">' + esc(s.dTag) + ' — no apps yet</div>' +
'</div>';
});
// Apps without any matching stack
var orphanApps = [];
apps.forEach(function(a) {
if (!stacks.some(function(s) { return s.dTag === a.category; })) {
orphanApps.push(a);
}
});
if (orphanApps.length > 0) {
contentHtml +=
'<div style="border:1px solid var(--border-color);border-radius:var(--border-radius);margin-bottom:10px;">' +
'<div style="font-weight:bold;font-size:14px;padding:10px 12px;border-bottom:1px solid var(--border-color);">Uncategorized <span style="font-size:12px;color:var(--muted-color);">(' + orphanApps.length + ')</span></div>';
orphanApps.forEach(function(a) {
contentHtml +=
'<div style="border-bottom:1px solid var(--border-color);font-size:12px;">' +
'<div style="padding:8px 12px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;" onclick="toggleAppDetail(this)">' +
'<span><span style="font-weight:bold;">' + esc(a.name) + '</span> <span style="color:var(--muted-color);">' + esc(appVersions[a.identifier] ? 'v' + appVersions[a.identifier].version : a.identifier) + '</span></span>' +
'<span style="font-size:11px;color:var(--muted-color);">▶</span>' +
'</div>' +
'<div class="appDetailBody" style="display:none;padding:0 12px 8px 12px;font-size:12px;color:var(--muted-color);">' +
'<div><span style="color:var(--color);">identifier:</span> ' + esc(a.identifier) + '</div>' +
'<div><span style="color:var(--color);">version:</span> ' + esc(appVersions[a.identifier] ? appVersions[a.identifier].version : '—') + '</div>' +
'<div><span style="color:var(--color);">pubkey:</span> ' + esc(a.pubkey) + '</div>' +
'<div><span style="color:var(--color);">repository:</span> ' + (a.repository ? '<a href="' + esc(a.repository) + '" target="_blank" rel="noopener">' + esc(a.repository) + '</a>' : '—') + '</div>' +
'<div><span style="color:var(--color);">description:</span> ' + esc(a.description || '—') + '</div>' +
'<div><span style="color:var(--color);">eventId:</span> ' + esc(a.eventId) + '</div>' +
'</div>' +
'</div>';
});
contentHtml += '</div>';
}
}
// Single wrapper div so #divBody flexbox sees only one child
divBody.innerHTML = '<div>' + createStackHtml + stacksHtml + formHtml + listHtml + '</div>';
divBody.innerHTML = '<div>' + createStackHtml + formHtml + contentHtml + '</div>';
}
// Toggle stack body collapse
window.toggleStackApps = function(header) {
var body = header.nextElementSibling;
var isHidden = body.style.display === 'none';
body.style.display = isHidden ? '' : 'none';
header.querySelector('span:last-child').textContent = isHidden ? '▼' : '▶';
};
// Toggle app detail expand
window.toggleAppDetail = function(row) {
var body = row.nextElementSibling;
var isHidden = body.style.display === 'none' || body.style.display === '';
body.style.display = isHidden ? 'block' : 'none';
row.querySelector('span:last-child').textContent = isHidden ? '▼' : '▶';
};
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function parseGiteaUrl(url) {
@@ -694,17 +757,20 @@
// Expose doPublish globally for onclick handler
window.doPublish = async function() {
const repoUrl = document.getElementById('pubRepoUrl').value.trim();
if (!repoUrl) { alert('Enter a Gitea repo URL.'); return; }
if (!repoUrl) { setStatus('Enter a Gitea repo URL.'); return; }
const parsed = parseGiteaUrl(repoUrl);
if (!parsed) { alert('Invalid repo URL. Use format: https://git.example.com/owner/repo'); return; }
if (!parsed) { setStatus('Invalid repo URL. Use format: https://git.example.com/owner/repo'); return; }
const stackSelect = document.getElementById('pubStack');
const stackDTag = stackSelect ? stackSelect.value : '';
if (!stackDTag) { setStatus('Select an app stack.'); return; }
// Derive name from repo name: my-cool-app -> My Cool App
const name = parsed.repo.replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
// Derive identifier from repo name: my-cool-app -> com.gitea.my_cool_app
const identifier = 'com.gitea.' + parsed.repo.replace(/-/g, '_');
const category = document.getElementById('pubCategory').value.trim() || 'uncategorized';
await publishAppDefinition(name, identifier, repoUrl, category, parsed.giteaPath);
await publishAppDefinition(name, identifier, repoUrl, stackDTag, parsed.giteaPath);
};
function subscribeAppDefinitions() {
@@ -724,7 +790,8 @@
console.log('[app-stacks] Added app def:', apps[apps.length - 1].name);
if (appDefsLoaded) renderApps();
});
window.addEventListener('ndkEose', () => {
window.addEventListener('ndkEose', (event) => {
if (appDefSub && event.detail && event.detail.subId !== appDefSub.subId) return;
if (!appDefsLoaded) { appDefsLoaded = true; renderApps(); }
});
}
@@ -759,11 +826,105 @@
console.log('[app-stacks] Added stack:', stacks[stacks.length - 1].name);
if (stacksLoaded) renderApps();
});
window.addEventListener('ndkEose', function() {
window.addEventListener('ndkEose', function(event) {
if (stackSub && event.detail && event.detail.subId !== stackSub.subId) return;
if (!stacksLoaded) { stacksLoaded = true; renderApps(); }
});
}
/* ================================================================
APP VERSION FUNCTIONS (kind 3063 Software Asset)
================================================================ */
function parseAppAsset(evt) {
const tags = evt.tags || [];
const identifier = getTagValue(tags, 'i') || '';
const version = getTagValue(tags, 'version') || '';
const size = getTagValue(tags, 'size') || '';
const urls = tags.filter(function(t) { return t[0] === 'url'; }).map(function(t) { return t[1]; });
return { identifier, version, size, url: urls[0] || '', created_at: evt.created_at || 0 };
}
// Direct WebSocket fetch of kind 3063 events from relay.zapstore.dev.
// Uses #i tag filtering to request only events for our specific apps.
// Bypasses NDK to avoid grouping/cache issues.
var ZAPSTORE_RELAY = 'wss://relay.zapstore.dev';
function subscribeAppAssets() {
// Wait for app definitions to load so we know the identifiers
if (!appDefsLoaded || apps.length === 0) {
setTimeout(subscribeAppAssets, 1000);
return;
}
console.log('[app-stacks] Fetching kind 3063 events for ' + apps.length + ' apps from ' + ZAPSTORE_RELAY);
fetchAppAssetsByIdentifier();
}
function fetchAppAssetsByIdentifier() {
var identifiers = apps.map(function(a) { return a.identifier; });
var batchSize = 50;
var batches = [];
for (var i = 0; i < identifiers.length; i += batchSize) {
batches.push(identifiers.slice(i, i + batchSize));
}
console.log('[app-stacks] Fetching in ' + batches.length + ' batches of up to ' + batchSize + ' apps each');
var batchIndex = 0;
function fetchNextBatch() {
if (batchIndex >= batches.length) {
if (!assetsLoaded) {
assetsLoaded = true;
renderApps();
}
console.log('[app-stacks] Kind 3063 fetch complete. Total events: ' + assetEventCount + ', unique apps with versions: ' + Object.keys(appVersions).length);
return;
}
var batch = batches[batchIndex];
batchIndex++;
var reqId = 'assets-' + batchIndex + '-' + Date.now();
console.log('[app-stacks] Fetching batch ' + batchIndex + '/' + batches.length + ' (' + batch.length + ' apps)...');
var ws = new WebSocket(ZAPSTORE_RELAY);
ws.onopen = function() {
ws.send(JSON.stringify(['REQ', reqId, { kinds: [3063], '#i': batch, limit: 500 }]));
};
ws.onmessage = function(ev) {
var msg = JSON.parse(ev.data);
if (msg[0] === 'EVENT' && msg[1] === reqId) {
var evt = msg[2];
assetEventCount++;
var parsed = parseAppAsset(evt);
if (parsed.identifier) {
var existing = appVersions[parsed.identifier];
if (!existing || parsed.created_at > existing.created_at) {
appVersions[parsed.identifier] = parsed;
if (assetsLoaded) renderApps();
}
}
} else if (msg[0] === 'EOSE' && msg[1] === reqId) {
ws.send(JSON.stringify(['CLOSE', reqId]));
ws.close();
console.log('[app-stacks] Batch ' + batchIndex + ' complete (total events: ' + assetEventCount + ', unique apps: ' + Object.keys(appVersions).length + ')');
if (!assetsLoaded) {
assetsLoaded = true;
renderApps();
}
setTimeout(fetchNextBatch, 100);
}
};
ws.onerror = function(err) {
console.error('[app-stacks] WebSocket error in batch ' + batchIndex + ':', err);
if (!assetsLoaded) {
assetsLoaded = true;
renderApps();
}
setTimeout(fetchNextBatch, 100);
};
}
fetchNextBatch();
}
async function publishAppStack(name, description) {
if (!isAuthenticated) { var ok = await promptLoginIfNeeded(); if (!ok) { setStatus('Sign in to publish.'); return; } }
var dTag = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
@@ -869,6 +1030,10 @@
// Set up app-stack event listener and subscription
initAppStackListener();
subscribeAppStacks();
// Fetch kind 3063 (Software Asset) events directly from relay.zapstore.dev
// for version info. Bypasses NDK to avoid grouping/cache issues.
subscribeAppAssets();
// Optional UX note for public mode pages.
if (!isAuthenticated && (authMode === 'optional' || authMode === 'none')) {
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.96",
"VERSION_NUMBER": "0.7.96",
"BUILD_DATE": "2026-08-03T22:46:27.122Z"
"VERSION": "v0.7.99",
"VERSION_NUMBER": "0.7.99",
"BUILD_DATE": "2026-08-04T20:24:55.694Z"
}
+157 -27
View File
@@ -90268,6 +90268,84 @@ function normalizeRelayUrl(relayUrl) {
}
}
/**
* Trailing-slash-tolerant relay lookup for ndk.pool.relays.
*
* NDK's internal normalizeRelayUrl() appends a trailing slash to every pool
* key (e.g. "wss://host/relay/"), while the worker's normalizeRelayUrl()
* (new URL().toString()) does NOT. This mismatch caused pool.get(noSlashUrl)
* to miss, so reconnect/data requests silently failed with "Relay not found
* in pool". This helper tries the exact key, the slash-appended form, and a
* canonical (slash-stripped) comparison across all pool entries.
*
* @param {string} relayUrl - URL as received from the page (may lack trailing slash)
* @returns {object|undefined} The NDKRelay instance, or undefined if not found
*/
function getRelayFromPool(relayUrl) {
if (!relayUrl || !ndk || !ndk.pool || !ndk.pool.relays) return undefined;
const direct = ndk.pool.relays.get(relayUrl);
if (direct) return direct;
let normalized = relayUrl;
try {
normalized = new URL(relayUrl).toString();
} catch (_error) {
normalized = String(relayUrl);
}
const slashForm = normalized.endsWith('/') ? normalized : normalized + '/';
const slashHit = ndk.pool.relays.get(slashForm) || ndk.pool.relays.get(normalized);
if (slashHit) return slashHit;
// Canonical fallback: compare with trailing slash stripped on BOTH sides.
const stripSlash = (u) => (typeof u === 'string' ? u.replace(/\/$/, '') : u);
const target = stripSlash(normalized);
for (const [poolUrl, poolRelay] of ndk.pool.relays.entries()) {
try {
if (stripSlash(new URL(poolUrl).toString()) === target) {
return poolRelay;
}
} catch (_error) {
// Ignore malformed pool URL entries
}
}
return undefined;
}
/**
* Trailing-slash-tolerant lookup of a relay's type ('read' | 'write' | 'both')
* from the relayTypes map. relayTypes stores the raw kind 10002 tag value
* (often without a trailing slash), so a no-slash/slash mismatch can hide the
* entry. Returns null when the relay is not in the user's relay list.
*
* @param {string} relayUrl
* @returns {string|null}
*/
function getRelayType(relayUrl) {
if (!relayUrl) return null;
const direct = relayTypes.get(relayUrl);
if (direct) return direct;
let normalized = relayUrl;
try {
normalized = new URL(relayUrl).toString();
} catch (_error) {
normalized = String(relayUrl);
}
const slashForm = normalized.endsWith('/') ? normalized : normalized + '/';
const slashHit = relayTypes.get(slashForm) || relayTypes.get(normalized);
if (slashHit) return slashHit;
const stripSlash = (u) => (typeof u === 'string' ? u.replace(/\/$/, '') : u);
const target = stripSlash(normalized);
for (const [storedUrl, storedType] of relayTypes.entries()) {
if (stripSlash(storedUrl) === target) return storedType;
}
return null;
}
function parseRelayFrame(message) {
if (Array.isArray(message)) return message;
if (typeof message !== 'string') return null;
@@ -96788,19 +96866,29 @@ function handleGetRelayData(requestId, port) {
// Keep original URL if parsing fails
}
let relay = ndk.pool.relays.get(normalizedUrl) || ndk.pool.relays.get(url);
// Trailing-slash-tolerant lookup: NDK pool keys are stored with a
// trailing slash (e.g. "wss://host/relay/") while relayTypes stores
// the raw tag value (often no slash). getRelayFromPool() reconciles
// both forms so the table gets the real relay.url and status instead
// of falling back to status: 0 with the no-slash URL.
let relay = getRelayFromPool(url) || getRelayFromPool(normalizedUrl);
// Defensive fallback: compare canonical URL strings across pool keys
if (!relay && normalizedUrl) {
for (const [poolUrl, poolRelay] of ndk.pool.relays.entries()) {
try {
if (new URL(poolUrl).toString() === normalizedUrl) {
relay = poolRelay;
break;
}
} catch (error) {
// Ignore malformed pool URL entries
}
// Write-only relays are intentionally excluded from the main pool by
// updateRelays(), so they would otherwise show status: 0 / "-" here.
// Add them to the pool on demand so they connect and the table can
// report their real status. (REQ frames are still blocked for
// write-only relays in attachRelayEventListeners, so this does not turn
// them into read relays.)
if (!relay && type === 'write' && typeof ndk.addExplicitRelay === 'function') {
const slashForm = normalizedUrl && normalizedUrl.endsWith('/')
? normalizedUrl
: (normalizedUrl || url) + '/';
try {
const added = ndk.addExplicitRelay(slashForm);
relay = added || getRelayFromPool(slashForm) || getRelayFromPool(normalizedUrl);
if (relay) attachRelayEventListeners(relay);
} catch (_addError) {
// Leave relay undefined; table falls back to status: 0 below.
}
}
@@ -96897,21 +96985,10 @@ function handleReconnectRelay(relayUrl, port) {
// Keep original relayUrl when parsing fails
}
let relay = ndk.pool.relays.get(normalizedRelayUrl) || ndk.pool.relays.get(relayUrl);
// Defensive fallback: compare canonical URL strings across pool keys
if (!relay && normalizedRelayUrl) {
for (const [poolUrl, poolRelay] of ndk.pool.relays.entries()) {
try {
if (new URL(poolUrl).toString() === normalizedRelayUrl) {
relay = poolRelay;
break;
}
} catch (error) {
// Ignore malformed pool URL entries
}
}
}
// Trailing-slash-tolerant lookup: NDK pool keys are stored with a trailing
// slash (e.g. "wss://host/relay/") while the page often passes the no-slash
// form. getRelayFromPool() handles both forms plus a canonical fallback.
let relay = getRelayFromPool(relayUrl) || getRelayFromPool(normalizedRelayUrl);
if (relay) {
const resolvedRelayUrl = relay.url || normalizedRelayUrl || relayUrl;
@@ -96965,6 +97042,59 @@ function handleReconnectRelay(relayUrl, port) {
});
}, 500);
} else {
// Relay is not in the main pool. Write-only relays are intentionally
// excluded from ndk.pool.relays (see updateRelays), so a manual
// reconnect request for one must add it to the pool on demand before
// we can connect it. This lets the relays.html reconnect button work
// for write-only relays like wss://laantungir.net/relay.
const relayType = getRelayType(relayUrl) || getRelayType(normalizedRelayUrl);
const isKnownWriteOnly = relayType === 'write';
if (isKnownWriteOnly && typeof ndk.addExplicitRelay === 'function') {
const slashForm = normalizedRelayUrl.endsWith('/')
? normalizedRelayUrl
: normalizedRelayUrl + '/';
console.log('[Worker] Write-only relay not in pool; adding on demand for reconnect:', slashForm);
try {
const added = ndk.addExplicitRelay(slashForm);
const addedRelay = added || getRelayFromPool(slashForm) || getRelayFromPool(normalizedRelayUrl);
if (addedRelay) {
attachRelayEventListeners(addedRelay);
const resolvedRelayUrl = addedRelay.url || slashForm;
const onConnect = () => {
console.log('[Worker] ✅ Write-only relay connected on demand:', resolvedRelayUrl);
addedRelay.off('connect', onConnect);
addedRelay.off('error', onError);
};
const onError = (error) => {
console.error('[Worker] ❌ Write-only relay error during on-demand connect:', resolvedRelayUrl, error);
broadcastRelayEvent(addedRelay.url, 'reconnect-error', {
name: error?.name || null,
message: error?.message || error?.toString?.() || String(error),
code: error?.code,
source: 'manual-reconnect-write-only'
}, 'relay');
addedRelay.off('connect', onConnect);
addedRelay.off('error', onError);
};
addedRelay.once('connect', onConnect);
addedRelay.once('error', onError);
markRelayConnectAttempt(resolvedRelayUrl);
broadcastRelayEvent(resolvedRelayUrl, 'reconnect-attempt', {
source: 'manual-reconnect-write-only',
targetUrl: normalizeRelayUrl(resolvedRelayUrl)
}, 'client');
addedRelay.connect().catch((e) => {
console.error('[Worker] ❌ Connect() threw error for write-only relay:', resolvedRelayUrl, e);
});
return;
}
} catch (addError) {
console.error('[Worker] Failed to add write-only relay to pool:', slashForm, addError);
}
}
console.warn('[Worker] Relay not found in pool:', relayUrl, 'normalized:', normalizedRelayUrl);
console.log('[Worker] Available relays:', Array.from(ndk.pool.relays.keys()));
}