Skip to content

Deepmarks v1.0.0

Latest

Choose a tag to compare

@ostermayer ostermayer released this 02 Jul 19:35

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). isPrivateIp misclassified
    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 undici Agent with a custom lookup to 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. the undici package's Agent) and even disagree on
    the connect.lookup callback shape — so the pin threw
    ERR_INVALID_IP_ADDRESS on one box and UND_ERR_INVALID_ARG on the
    other, breaking metadata previews, favicon-on-miss, and direct-file
    downloads. safeFetch now 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:0 profiles 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:0 for 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 enforced http(s); the API/search/public-cache
    conversions passed the URL through as a bare string, so a kind:39701
    published straight to the relay with a javascript:/data: d-tag
    could reach an <a href> and run in our origin. A javascript: 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:9735 receipt, 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
    redelivered invoice_updated can retry — while still blocking a double
    publish.
  • The /publish fan-out drain is crash-safe. The server-mediated
    publish worker used a plain BRPOP, 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.