Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8979e47489 | ||
|
|
d4dfd31263 | ||
|
|
b0233f8bc3 |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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) + '…' + 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
@@ -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
@@ -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()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user