Skip to content

feat: add private address inboxes and Zig CLI#2

Open
EmmanuelKeifala wants to merge 9 commits into
abdullah4tech:mainfrom
EmmanuelKeifala:feat/private-address-inboxes
Open

feat: add private address inboxes and Zig CLI#2
EmmanuelKeifala wants to merge 9 commits into
abdullah4tech:mainfrom
EmmanuelKeifala:feat/private-address-inboxes

Conversation

@EmmanuelKeifala

@EmmanuelKeifala EmmanuelKeifala commented Jul 17, 2026

Copy link
Copy Markdown

Summary

This adds a native Zig CLI for Ashdrop and extends recipient-keyed drops with private address inboxes. A recipient can create a local receive identity, share encrypted environment files, pull a drop safely, and list pending drops with ashdrop inbox without making inbox activity enumerable from the public receive address alone.

How private inboxes work

The API creates and persists a dedicated P-256 inbox keypair in SQLite. The CLI combines its existing receive private key with the API inbox public key through ECDH, derives a separate inbox MAC key with HKDF-SHA-256, and signs a short-lived canonical listing request with HMAC-SHA-256. The API independently derives and verifies the same proof before returning inbox metadata.

The listing exposes only drop IDs, expiry times, and remaining views. It never returns ciphertext, IVs, ephemeral keys, sender identity, notification tokens, recipient private keys, or plaintext. The public receive address by itself is not enough to list pending drops.

Included changes

  • Adds atomic metadata/open endpoints so recipient drops cannot bypass view limits through the legacy fetch route.
  • Stores recipient public keys and migrates the web recipient flow to verify identity before consuming a drop.
  • Adds a Zig CLI with address, share, pull, and inbox commands.
  • Implements browser-compatible P-256 ECDH, HKDF-SHA-256, and AES-256-GCM encryption in Zig.
  • Protects local identity and output files with strict permissions, no-follow path handling, atomic writes, and safe overwrite behavior.
  • Adds a persistent API inbox key, recipient inbox index, proof verification, response privacy headers, and bounded per-IP rate limiting.
  • Supports managed and self-hosted endpoints, requiring HTTPS for inbox proofs except on loopback development addresses.
  • Configures the Render deployment to trust its load balancer for client-IP rate limiting.

Compatibility and migration

Existing legacy drops remain readable through the original fetch-and-burn flow. New recipient-keyed drops use metadata plus atomic open. Existing databases are migrated in place with default values for the new recipient and inbox metadata columns.

Verification

  • go test ./...
  • pnpm run check
  • pnpm run build
  • zig build test
  • zig build -Doptimize=ReleaseSafe
  • git diff --check

Automated feature tests are contained in the Zig CLI test suite; the Go API and web changes are verified through compilation, static checks, production build, and end-to-end CLI/API flows.

Summary by CodeRabbit

  • New Features

    • Added recipient-keyed drops with inbox support, including inbox key retrieval, inbox listing, and recipient-aware metadata/open behavior.
    • Expanded API responses with remaining views and additional metadata, plus a metadata endpoint.
    • Introduced a Zig-based CLI for sharing, pulling (atomic output), and inbox browsing; updated share/pull URI/link formats and endpoint overrides.
  • Security & Reliability

    • Improved request throttling and trust-proxy client IP handling; tightened input/response validation and encrypted-payload handling.
    • Updated web reveal flow to verify recipient keys and open correctly.
  • Documentation / Chores

    • Added CLI README, updated Zig build setup/versioning, adjusted Render trust-proxy config, and refined ignore rules.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

@EmmanuelKeifala is attempting to deploy a commit to the Abdullah Mustapha's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds recipient-keyed encrypted drops across the Go API, SQLite store, Zig CLI, and web app. It introduces inbox proofs and listing, secure local identities, bounded API clients, safe output handling, recipient-aware reveal flows, and related build, test, documentation, and deployment configuration.

Changes

Recipient-Keyed Drop System

Layer / File(s) Summary
Backend storage and inbox API
ashdrop/api/main.go, ashdrop/api/store.go, ashdrop/api/main_test.go
The backend stores recipient and ephemeral keys, exposes metadata and inbox endpoints, verifies inbox proofs, manages inbox keys, and applies route-specific rate limiting.
CLI cryptography and receive identity
ashdrop/cli/src/crypto.zig, ashdrop/cli/src/crypto_test.zig, ashdrop/cli/src/identity.zig
The CLI implements P-256 ECDH encryption, decryption, inbox proofs, protocol validation, and secure identity persistence.
CLI HTTP client and references
ashdrop/cli/src/api.zig, ashdrop/cli/src/config.zig, ashdrop/cli/src/links.zig
The CLI adds bounded API requests, typed metadata/open/inbox responses, endpoint validation, and parsing and formatting for drop and receive references.
CLI commands and safe output
ashdrop/cli/src/commands.zig, ashdrop/cli/src/files.zig, ashdrop/cli/src/main.zig
The CLI wires address, share, pull, and inbox commands with recipient checks, atomic output handling, path safety, and command diagnostics.
Web recipient-keyed flow
ashdrop/web/src/lib/api.ts, ashdrop/web/src/routes/drop-for/[pubkey]/+page.svelte, ashdrop/web/src/routes/s/[id]/+page.svelte
The web app sends recipient public keys, opens recipient-keyed secrets on reveal, decrypts with the local key, and reports key mismatches.
CLI packaging and supporting configuration
ashdrop/cli/build.zig, ashdrop/cli/build.zig.zon, ashdrop/cli/README.md, ashdrop/cli/testdata/*, ashdrop/cli/src/test_server.zig, .gitignore, render.yaml
Build metadata, protocol fixtures, test-server support, CLI documentation, ignore rules, and proxy/persistence configuration are added or updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Sender
  participant CLI
  participant API
  participant Store
  participant Recipient
  Sender->>CLI: encrypt plaintext for recipient
  CLI->>API: create recipient-keyed drop
  API->>Store: persist encrypted payload and key metadata
  Recipient->>API: request inbox key and submit proof
  API->>Store: list recipient-keyed drops
  Store-->>API: return inbox metadata
  API-->>Recipient: return drop identifiers and view counts
  Recipient->>API: request metadata and open payload
  API->>Store: consume eligible view
  Store-->>API: return encrypted payload
  API-->>Recipient: return ciphertext, IV, and ephemeral key
  Recipient->>CLI: decrypt locally
Loading

Possibly related PRs

Suggested reviewers: abdullah4tech

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: private address inboxes and the new Zig CLI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ashdrop/api/main.go`:
- Around line 106-111: Update the recipient-key validation around validPublicKey
so recipientPub and ephemeralPub must either both be present or both absent.
When supplied, validate both keys as valid on-curve P-256 points, not merely
correctly shaped keys, before persisting them. Preserve the existing bad-request
response for invalid key input and ensure Store.Fetch cannot expose an unmatched
ephemeral key.

In `@ashdrop/cli/src/config.zig`:
- Around line 5-9: Restore the production HTTPS endpoint defaults in the
managed_api and managed_web configuration constants, or ensure the release build
injects those deployed values instead of localhost URLs. Update corresponding
tests to assert the managed endpoints while preserving override behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8654c6f2-7fe6-416c-8c73-af1b1bc379e9

📥 Commits

Reviewing files that changed from the base of the PR and between 772497a and 25176af.

📒 Files selected for processing (23)
  • .gitignore
  • ashdrop/api/main.go
  • ashdrop/api/store.go
  • ashdrop/cli/README.md
  • ashdrop/cli/build.zig
  • ashdrop/cli/build.zig.zon
  • ashdrop/cli/src/api.zig
  • ashdrop/cli/src/commands.zig
  • ashdrop/cli/src/config.zig
  • ashdrop/cli/src/crypto.zig
  • ashdrop/cli/src/crypto_test.zig
  • ashdrop/cli/src/files.zig
  • ashdrop/cli/src/identity.zig
  • ashdrop/cli/src/links.zig
  • ashdrop/cli/src/main.zig
  • ashdrop/cli/src/test_server.zig
  • ashdrop/cli/test.zig
  • ashdrop/cli/testdata/generate-protocol-v1.mjs
  • ashdrop/cli/testdata/protocol-v1.json
  • ashdrop/web/src/lib/api.ts
  • ashdrop/web/src/routes/drop-for/[pubkey]/+page.svelte
  • ashdrop/web/src/routes/s/[id]/+page.svelte
  • render.yaml

Comment thread ashdrop/api/main.go Outdated
Comment thread ashdrop/cli/src/config.zig Outdated
Comment thread ashdrop/cli/build.zig.zon Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ashdrop/cli/src/config.zig (1)

26-33: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace std.Io.net.HostName.fromUri here. The pinned Zig snapshot doesn’t expose that API; uri.getHost/HostName.init is the available path, so this won’t compile as written.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ashdrop/cli/src/config.zig` around lines 26 - 33, Replace the unavailable
std.Io.net.HostName.fromUri call in the endpoint parsing flow with the supported
uri.getHost and std.Io.net.HostName.init path. Preserve the existing successful
hostname return and InvalidEndpoint handling for missing or invalid hosts and IP
literals.
🧹 Nitpick comments (1)
ashdrop/cli/src/config.zig (1)

38-48: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover embedded endpoint precedence.

Add an assertion that an embedded endpoint wins over the environment value; the current test never exercises that branch.

Proposed test
     try std.testing.expectEqualStrings(
         "https://env.example",
         try resolveApi(null, "https://env.example", null),
     );
+    try std.testing.expectEqualStrings(
+        "https://embedded.example",
+        try resolveApi(null, "https://env.example", "https://embedded.example"),
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ashdrop/cli/src/config.zig` around lines 38 - 48, Add a case to the "ordinary
API references prefer explicit flag then environment" test that calls resolveApi
with no explicit flag, a populated environment value, and the embedded endpoint,
asserting the embedded endpoint is returned. Keep the existing precedence
assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ashdrop/cli/src/config.zig`:
- Around line 26-33: Replace the unavailable std.Io.net.HostName.fromUri call in
the endpoint parsing flow with the supported uri.getHost and
std.Io.net.HostName.init path. Preserve the existing successful hostname return
and InvalidEndpoint handling for missing or invalid hosts and IP literals.

---

Nitpick comments:
In `@ashdrop/cli/src/config.zig`:
- Around line 38-48: Add a case to the "ordinary API references prefer explicit
flag then environment" test that calls resolveApi with no explicit flag, a
populated environment value, and the embedded endpoint, asserting the embedded
endpoint is returned. Keep the existing precedence assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 414702ae-6d21-489b-ba3b-6048407b7afa

📥 Commits

Reviewing files that changed from the base of the PR and between 25176af and 4290935.

📒 Files selected for processing (2)
  • ashdrop/api/main.go
  • ashdrop/cli/src/config.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • ashdrop/api/main.go

@abdullah4tech abdullah4tech left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — really solid work. I pulled the branch and verified everything locally before reviewing.

Local verification

Check Result
go vet / go test ./... clean / no test files exist (see below)
zig build test + zig build -Doptimize=ReleaseSafe pass
pnpm check / pnpm build 0 errors / pass
E2E: address createshareinboxpull works; pulled file byte-identical; identity dir 700, key file & output 600
One-view burn second pull rejected, inbox empties
Race: 20 concurrent POST /open on a 1-view drop exactly 1 response got the ciphertext
Wrong-recipient pull rejected without consuming the view; owner could still pull afterward
Bogus inbox proof / legacy GET on recipient drop 401 / 404 as designed

What I liked

This is unusually careful: canonical base64 validation everywhere (blocks alternate encodings of the same key), constant-time proof comparison, key material zeroed after use, symlink-refusing path resolution for identity and output files, atomic writes that refuse late-created destinations, a bounded no-redirect HTTP client that never echoes server error text, and the fixture test pinning the Zig crypto to Node Web Crypto output. The web recipient flow correctly checks metadata before spending a view, and legacy drops remain readable.

Blocking

  1. .gitignore adds docs/* — this looks unrelated to the PR and would silently hide any future docs/ directory from git. Please drop it.
  2. No Go test coverage. The API package has no test files, so the go test ./... checkbox is vacuous. All the new server logic (atomic Open, HMAC proof verification, rate limiter, inbox key persistence) is untested. More importantly, the open-race safety I verified depends entirely on the pre-existing db.SetMaxOpenConns(1)Open()'s SELECT-then-UPDATE inside a deferred transaction is not intrinsically race-safe. If the store ever moves to a pooled DB (the comments mention Redis/Postgres later), one-view drops become double-readable. At minimum a regression test for concurrent one-view opens, and ideally a guard like UPDATE ... SET views = views + 1 WHERE id = ? AND views < max_views so correctness doesn't hinge on pool size.

Minor / non-blocking

  1. Proof replay window: inbox proofs are bound to path+limit+timestamp but carry no nonce, so a captured proof replays for ±5 min. HTTPS-only enforcement keeps this low severity — just noting it.
  2. Rate limiter fail-closed: once the 4096-window map fills, new IPs are denied, so an attacker with many IPs can briefly lock legitimate new clients out of creating drops. Each request also sweeps the whole map under the mutex — fine at this scale, worth revisiting if traffic grows.
  3. /metadata reveals recipientPub to anyone holding the drop ID. The web flow needs it, but any link intermediary learns which receive address a drop targets. Possible future hardening: client submits its pubkey and gets a boolean match back.
  4. Zig nit: inboxProof's 160-byte request_buffer has a worst-case canonical string of ~163 bytes (limit=100 plus a 19-digit at), and overflow hits catch unreachable. Unreachable with real clock values, but sizing the buffer to 192 makes it safe by construction rather than by accident.
  5. Cached old web clients will show "gone" for new recipient-keyed drops (legacy GET now 404s them) until users reload the SPA — transient, acceptable.

One housekeeping note: this PR supersedes #1 (same base features plus the inbox work), so I'll close #1 when this lands.

@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ashdrop Ready Ready Preview, Comment Jul 18, 2026 4:25am

@EmmanuelKeifala

Copy link
Copy Markdown
Author

@abdullah4tech this ".gitignore adds docs/* — this looks unrelated to the PR and would silently hide any future docs/ directory from git. Please drop it." should be non-blocking, as docs is my local store where i propose and keep implementation plans, and that is totally not need in this remote origin. what i will do is remove entirely from the ignore file

@EmmanuelKeifala

Copy link
Copy Markdown
Author

addressed the blocking items removed the docs ignore added go coverage for every cli api route and its security and concurrency edges and changed open to claim views atomically with update returning so pooled concurrent requests yield exactly one final payload also increased the zig inbox proof buffer and added full width timestamp coverage

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ashdrop/api/store.go (1)

71-86: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Run every column migration before creating the recipient index, and migrate opened_at.

On an existing database, executing schema first attempts to create idx_secrets_recipient_inbox while recipient_pub and created_at are still absent, so startup fails before this migration loop runs. The loop also omits the newly introduced opened_at, which subsequently breaks metadata/open queries.

Remove the inbox index creation from schema, add the missing migration, then create the index at Line 86. Add an upgrade test starting from the legacy schema.

Proposed migration fix
 	for _, migration := range []string{
 		`ALTER TABLE secrets ADD COLUMN pin_protected INTEGER NOT NULL DEFAULT 0`,
 		`ALTER TABLE secrets ADD COLUMN ephemeral_pub TEXT NOT NULL DEFAULT ''`,
 		`ALTER TABLE secrets ADD COLUMN recipient_pub TEXT NOT NULL DEFAULT ''`,
 		`ALTER TABLE secrets ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0`,
+		`ALTER TABLE secrets ADD COLUMN opened_at INTEGER`,
 	} {

Also remove CREATE INDEX ... idx_secrets_recipient_inbox from the initial schema string; Line 86 already creates it after migration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ashdrop/api/store.go` around lines 71 - 86, Update the schema initialization
and migration flow in the store setup: remove idx_secrets_recipient_inbox
creation from the initial schema string, add the opened_at column to the
migration loop, and keep the index creation after all migrations complete. Add
an upgrade test that initializes from the legacy schema and verifies startup and
migrated metadata/open queries succeed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ashdrop/api/store.go`:
- Around line 71-86: Update the schema initialization and migration flow in the
store setup: remove idx_secrets_recipient_inbox creation from the initial schema
string, add the opened_at column to the migration loop, and keep the index
creation after all migrations complete. Add an upgrade test that initializes
from the legacy schema and verifies startup and migrated metadata/open queries
succeed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 42b6af90-3533-4aff-9063-e668b72c8f8e

📥 Commits

Reviewing files that changed from the base of the PR and between 69d848f and aabe916.

📒 Files selected for processing (5)
  • .gitignore
  • ashdrop/api/main_test.go
  • ashdrop/api/store.go
  • ashdrop/cli/src/crypto.zig
  • ashdrop/cli/src/crypto_test.zig
💤 Files with no reviewable changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • ashdrop/cli/src/crypto_test.zig
  • ashdrop/cli/src/crypto.zig

@EmmanuelKeifala

Copy link
Copy Markdown
Author

checked this against the full store history the recipient inbox index is already created only after migrations and opened at has existed in the secrets schema since the first api commit so there is no supported legacy schema missing that column and no migration change is needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants