Repository navigation
10 portal
Design note + implementation record for the Osiris-style serverless portal. The non-negotiable principle from
08-hidden-services.md§8.6 holds for everything below: browser = application, service = data.
A portal is a view the browser computes, not a place the browser visits:
- Services store opaque signed bytes and verify signatures. They never execute logic on behalf of users, never learn who asked (SURB replies), and never see plaintext they weren't meant to (E2E payloads stay sealed).
- The browser holds keys, identity, follows, reputation, and rendering.
Fetched bytes render in
sandbox=""iframes or as app UI over parsed data. - Protocol support is explicit and versioned: the client speaks exactly the
envelope/object types in
portal-data, nothing else.
The windowed UI (web/src/adapters/driving/) is the physical expression of this: the main
view is the portal context (page + Social), while transport machinery
(Connection, Messages, Fetch, Log) lives in closable panels. New portal
features arrive as toolbar entries + panels + pure-logic modules, never as
service-side code running in the browser.
| Layer | Crate / module | Status |
|---|---|---|
| Data model |
crates/portal-data (objects, logs, LWW-map) |
✅ 4 tests |
| Sync core |
crates/portal-replication (heads, wants, verified apply) |
✅ 4 tests + 5 golden wire vectors |
| Provider |
services/portal-provider/ (object/log store, 6 routes) |
✅ 5 tests |
| Replication | client-driven gossip over the routes above | provider side ✅, browser TS port ✅ (web/src/domain/portalSync.ts + portalVerify.ts + portalSyncIo.ts) |
| Discovery | signed invite links + local contacts + URI-bar binding | ✅ |
| Reputation | client PoW + web-of-trust + local scores | ✅ (this section: PoW, local scores, rate limits) |
Discovery starts — and, for now, ends — with signed introductions:
link = "nym://<id>.<enc>@<gw>#invite=<base64url(JSON)>"
signed = domain || svc_identity || svc_encryption || svc_gateway || inviter || note
-
crates/nym-hidden-service/src/invite.rs: sign/verify/compact-codec, including a cross-implementation test (a TypeScript-signed vector verified in Rust). - Browser:
signInvite/verifyInviteinweb/src/domain/identity.ts(byte-exact mirror), pure codec inhiddenService.mjs,parseInviteLink()splitting address + path + invite. - URI bar: pasting an invite link fetches the service and raises a banner
showing introducer + note. Accepting verifies the signature first, then
saves a petname into the local-only Contacts panel (
web/src/adapters/driving/shell/). Bait-and-switch (invite naming another service) is refused twice: client-side shape check and signature verification. - The fragment never goes on the wire — invites authenticate the introducer to the recipient only. Notes are capped at 140 chars so links stay small.
What this deliberately omits (per 05-security.md §5.7): any global directory,
DHT, or ambient peer discovery. Contacts never leave the machine.
Every portal datum is an Object:
signing_bytes = b"fly-portal-v1/object" || author(32) || kind || 0x00 || payload
id = SHA256(signing_bytes || sig)
-
kindis namespaced text (post,profile,dm-chunk,wiki/…), charset[a-z0-9/_-], max 64 chars. Unknown kinds are stored (any peer can hold bytes) but only rendered if the client knows the kind. -
idbinds author + kind + payload + signature: tampering any byte changes the id, so references (feeds, replies, log entries) are self-checking. - JSON form carries hex ids/keys and base64 payloads, matching the envelope
conventions of
nym-hidden-service.
Append-only logs order one author's objects: each entry binds
(author, seq, obj_id) under fly-portal-v1/log-entry. Ingest rule: seq
must equal the log length — forks (two entries, same seq) are rejected, never
merged. A log is a portable, replayable identity history.
LWW-map converges shared mutable state (profiles, wiki pages, pin lists)
without coordination: each key holds the value with the greatest
(day, author, obj_hash) dot. Merge is union + max, hence commutative,
associative, and idempotent — any two replicas that exchange entries converge.
Wall-clock dependence is limited to coarse days, consistent with the metadata
policy (09-social.md §9.2).
Deliberate omissions: no timestamps below day granularity, no global sequence, no delete propagation (tombstones are just newer values), no access control beyond signatures. Spam and Sybil are not solved here — see §10.5.
The portal provider stores objects by id, appends to logs after verifying
signatures and continuity, and answers GET /obj/<id>, GET /log/<author>?since=,
POST /obj, POST /log-entry — data + verification only, via the existing
HiddenService::dispatch harness. Replication is client-driven gossip:
browsers exchange want-lists/have-lists through SURB replies and converge
LWW state locally. The serve shapes for the two read routes
(GET /heads, GET /log/<author>?since=) live in
crates/portal-replication/src/serve.rs and are pinned byte-for-byte by the
golden vectors in crates/portal-replication/tests/golden.rs, which the
browser TypeScript port mirrors exactly: web/src/domain/portalSync.ts
parses the same GET /heads / GET /log/<author>?since= JSON, reproduces
entry_bytes and object signing bytes byte-for-byte, computes wants as the
local log head per author, and applies batches atomically after verifying
signatures (web/src/domain/portalVerify.ts). The browser port is a
read-side replica only — it never merges forks, enforces continuity, and
persists under the frozen fly.portal.sync.v1 key. Traffic rides
fetchNym (serialized) / fetchNymParallel (tag-correlated) NYM requests
via web/src/adapters/driven/portalSyncIo.ts, surfaced in the Portal panel
(web/src/adapters/driving/portal/PortalSync.tsx). Discovery starts
where we already are — invite links
(nym:// URIs + petnames) and URI-bar binding — before any ambient mechanism.
Three layers, each insufficient alone, strong together:
-
Client-side PoW (
portal-reputation/src/pow.rs,web/src/mixnet/pow.mjs):SHA256(domain || author || payload_hash || nonce)with N leading zero bits. Binding author + payload hash makes proofs non-replayable across authors or messages; verification is one hash. Providers advertise difficulty inGET /(pow_bits, 0 = off, capped at 32 so no provider can demand years of CPU). The browser proves only when required, after reading the descriptor. PoW prices spam; it does not stop a funded attacker. -
Local reputation (
portal-reputation/src/reputation.rs,web/src/mixnet/reputation.mjs): scores from first-hand observation only (valid/invalid signatures, duplicates, undecryptables, equivocation), with clamping and 30-day decay. Never fetched, never published. The timeline verifies every post signature on receipt — forgeries never render — and shows the local standing per author. -
Provider rate limits (
portal-reputation/src/rate.rs, wired into both providers): per-author daily budgets (PORTAL_RATE_PER_DAY,SOCIAL_RATE_PER_DAY, default 500; DMs bucketed per recipient so one victim's mailbox can't be flooded). In-memory by design (backstop, not accounting); 429 on exhaustion.
Enforcement map: POST /obj and /log-entry (portal) and /post, /profile,
/dm (social) check PoW-then-budget before touching storage. Unsigned,
under-proofed, or over-budget writes get 400/429.
⚠️ Experimental / pre-audit. This repository is under active development and has not had an independent security audit. The Sphinx core incrates/is a reference implementation, never production crypto. Do not rely on it for high-stakes anonymity yet — see the threat model. Apache-2.0 © 2026 odelyzid (tribewarez). Report a security issue.
Nysiris docs
- Home
- 1. High-Level Architecture
- 2. Addressing & Routing Deep-Dive: how "DNS" works in Sphinx/Nym
- 3. Browser & Mobile Client (Web, Android, Desktop)
- 4. Hosting a Web Server / Service Reachable via the Nym Mixnet
- 5. Security Analysis & Recommendations
- 6. Implementation Roadmap
- 7. Desktop on Linux Mint / Debian
- 8. Hidden-Service Abstraction
- 9. nysiris-social: Metadata-Minimal Microblog + Encrypted DMs
- 10. Portal Layers: Data, Replication, Discovery, Reputation
- 11. UX Redesign — Calm, Private-by-Default Client
- 12. Windows build (MSYS2 / MSVC)
- 13. Hosting SDK &
nysirisCLI