Deepmarks 1.0.0
Deepmarks 1.0.0 is the stable 1.0 milestone for the public source
stream, graduating from the v0.9.x line. It is a security-hardening and
reliability release: a full code review of the API, archive worker,
bunker, frontend, and extension, with the resulting fixes shipped and
verified in production, plus a documentation accuracy pass across every
doc and README. Nothing about the user's data model, keys, or portability
changes — every bookmark you save is still a signed Nostr event you can
read from any client, exactly as before.
Ships alongside Android / Zapstore build 2.2.11 and the browser extension
2.2.8. bunker/ is unchanged; no Box C redeploy is required.
SSRF guard: IPv6-literal bypass closed, connect-pin reverted
The API and archive worker both fetch user-submitted URLs from a
privileged network position (they can reach Redis, the bunker's relay
path, and the cloud metadata service), so the shared SSRF guard is
load-bearing. Two corrections:
- IPv6-literal bypass (critical).
isPrivateIpmisclassified
bracketed IPv6 literals as public. The archive-worker copy never
stripped the[..]brackets (so[::1],[fd00::1],
[::ffff:10.0.0.4]all read as public), and both copies only matched
IPv4-mapped addresses in dotted form while the URL parser normalizes
them to hex ([::ffff:a00:4]) — so a mapped literal reached internal
IPv4 targets over a dual-stack connect. The guard now strips brackets
and folds IPv4-mapped/-compatible IPv6 (hex or dotted) down to the
embedded v4 before the private-range checks, on both boxes. - Connect-level IP pinning reverted. 0.9.0 pinned the resolved IP
into an undiciAgentwith a customlookupto defend DNS rebinding.
That pin only works when the dispatcher and the fetch come from the
same undici, but the two boxes run different undici versions (Node's
built-in fetch vs. theundicipackage'sAgent) and even disagree on
theconnect.lookupcallback shape — so the pin threw
ERR_INVALID_IP_ADDRESSon one box andUND_ERR_INVALID_ARGon the
other, breaking metadata previews, favicon-on-miss, and direct-file
downloads.safeFetchnow validates, DNS-resolves, rejects any private
resolved address, and fetches — with redirects followed manually and
re-validated per hop, which closes the redirect-based SSRF. The
DNS-rebinding TOCTOU is a tracked follow-up (it needs a version-matched
undici fetch+Agent), documented in
safe-url.ts.
The /metadata oEmbed sub-fetch and the archive-rescue candidate verify
both previously followed redirects with no per-hop re-check; both now walk
redirects manually through the guard.
Forged-profile and bookmark-URL hardening
- Ingested
kind:0profiles are signature-verified. The
follows-ingester fetches curators' profile metadata from their external
relays over a raw WebSocket. A hostile relay could return a forged
kind:0for an arbitrary victim pubkey with an attacker-chosen
name/nip05/lud16, poisoning the cached profile and redirecting
manual zaps. The profile cache now re-verifies the event signature at
the sink and drops it if it fails. - Bookmark URLs are scheme-checked at every render sink. Only the
relay-parse path enforcedhttp(s); the API/search/public-cache
conversions passed the URL through as a bare string, so akind:39701
published straight to the relay with ajavascript:/data:d-tag
could reach an<a href>and run in our origin. Ajavascript:URL is
now neutralized at every render sink, and non-http(s)bookmark URLs
are rejected server-side before they enter the first-paint cache or the
search index.
Payment + delivery durability
- Zap receipts survive a transient bunker outage. The settlement
handler consumed (deleted) the pending zap before the bunker signed the
kind:9735receipt, so a brief signing failure dropped the receipt
permanently. It now atomically claims the pending record, finalizes
it only after signing succeeds, and releases the claim on failure so a
redeliveredinvoice_updatedcan retry — while still blocking a double
publish. - The
/publishfan-out drain is crash-safe. The server-mediated
publish worker used a plainBRPOP, so a signed event lived only in
worker memory between the pop and the forward to the relay — a hard
crash there lost a save the client had already been 202'd for. It now
BLMOVEs each event into a per-worker processing list and recovers a
dead worker's list on the next boot (heartbeat-gated), mirroring the
archive queue's crash-safe pattern. Re-forwards are safe: the relay
dedupes by event id.
Self-service from the web app
The API's CORS policy allowed only GET/POST, so the browser blocked
the cross-origin DELETE/PUT calls the web app makes to revoke an API
key, delete an account, release a username, or save settings. It now
allows PUT/DELETE/OPTIONS, so those self-service actions work from
the web again.
Documentation
Every doc and component README was audited against the current code:
refreshed test counts (1,235 passing + 74 env-gated) and version
narrative, corrected the DELETE /account/archives API reference
(response shape, refcount-gated deletion, mirror-delete queue,
limit/offset caps), documented the Box B WireGuard residential-egress
sidecar, the crash-safe fan-out drain, the signature-verified profile
cache, and the Cerebras LLM fallback, and fixed the relay kind-list and
kind:1 persistence-exception descriptions.
What's next
The DNS-rebinding connect-pin (a version-matched undici fetch+Agent),
the extension's NIP-46/NIP-07 login project, and the worker
streaming-encrypt pass remain open. See
reliability-2026-06.md.
Public source mirror and release tag policy: see
release.md and versioning.md.