diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index bfe344a94..30831a0e7 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -409,11 +409,8 @@ jobs: DB_DATABASE=cipherbox_staging JWT_SECRET=${{ secrets.JWT_SECRET }} CORS_ALLOWED_ORIGINS=${{ vars.CORS_ALLOWED_ORIGINS }} - IPFS_PROVIDER=local - IPFS_LOCAL_API_URL=http://ipfs:5001 - IPFS_LOCAL_GATEWAY_URL=http://ipfs:8080 - DELEGATED_ROUTING_URL=http://someguy:8190 - DELEGATED_ROUTING_FALLBACK_URL=https://delegated-ipfs.dev + KUBO_API_URL=http://ipfs:5001 + ROUTING_V1_URL=http://someguy:8190 THROTTLE_BYPASS_SECRET=${{ secrets.THROTTLE_BYPASS_SECRET }} GRAFANA_LOKI_URL=${{ vars.GRAFANA_LOKI_URL }} GRAFANA_LOKI_USERNAME=${{ vars.GRAFANA_LOKI_USERNAME }} diff --git a/.github/workflows/desktop-e2e.yml b/.github/workflows/desktop-e2e.yml index a0d691869..9e1eca804 100644 --- a/.github/workflows/desktop-e2e.yml +++ b/.github/workflows/desktop-e2e.yml @@ -112,21 +112,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - # --- Build desktop frontend (Tauri embeds from frontendDist) --- - - - name: Build desktop frontend - run: | - pnpm --filter @cipherbox/crypto build - pnpm --filter @cipherbox/core build - pnpm --filter @cipherbox/api-client build - pnpm --filter @cipherbox/sdk-core build - pnpm --filter @cipherbox/sdk build - cd apps/desktop - pnpm vite build - env: - VITE_API_URL: http://localhost:3000 - VITE_TEST_LOGIN_SECRET: e2e-test-secret-ci-only - # --- Build debug binary --- - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -214,44 +199,6 @@ jobs: sleep 1 done - - name: Install Redis (macOS) - if: runner.os == 'macOS' - run: | - brew install redis - brew services start redis - - - name: Install Redis (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get install -y redis-server - sudo systemctl start redis-server - - - name: Install Redis (Windows) - if: runner.os == 'Windows' - shell: powershell - run: | - choco install memurai-developer -y --no-progress - # Memurai installs as a Windows service and starts automatically - $svc = Get-Service -Name "Memurai" -ErrorAction SilentlyContinue - if ($svc -and $svc.Status -eq "Running") { - Write-Host "Memurai service is running" - } else { - # Fallback: start service manually - Start-Service -Name "Memurai" -ErrorAction SilentlyContinue - Start-Sleep -Seconds 3 - } - # Verify Redis is responding - $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + $env:PATH - $ready = $false - for ($i = 0; $i -lt 10; $i++) { - try { - $result = & redis-cli ping 2>$null - if ($result -eq "PONG") { $ready = $true; break } - } catch {} - Start-Sleep -Seconds 1 - } - if ($ready) { Write-Host "Redis (Memurai) ready" } else { Write-Host "WARNING: Redis may not be ready" } - # --- Build backend packages --- - name: Build mock-ipns-routing @@ -274,12 +221,8 @@ jobs: DB_DATABASE=cipherbox_test JWT_SECRET=desktop-e2e-jwt-secret-key CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:1420 - IPFS_PROVIDER=local - IPFS_LOCAL_API_URL=http://localhost:5001 - IPFS_LOCAL_GATEWAY_URL=http://localhost:8080 - DELEGATED_ROUTING_URL=http://localhost:3001 - REDIS_HOST=localhost - REDIS_PORT=6379 + KUBO_API_URL=http://localhost:5001 + ROUTING_V1_URL=http://localhost:3001 TEST_LOGIN_SECRET=e2e-test-secret-ci-only ACCESS_TOKEN_TTL=2h ENVEOF @@ -337,12 +280,8 @@ jobs: DB_DATABASE: cipherbox_test JWT_SECRET: desktop-e2e-jwt-secret-key CORS_ALLOWED_ORIGINS: http://localhost:5173,http://localhost:1420 - IPFS_PROVIDER: local - IPFS_LOCAL_API_URL: http://localhost:5001 - IPFS_LOCAL_GATEWAY_URL: http://localhost:8080 - DELEGATED_ROUTING_URL: http://localhost:3001 - REDIS_HOST: localhost - REDIS_PORT: 6379 + KUBO_API_URL: http://localhost:5001 + ROUTING_V1_URL: http://localhost:3001 TEST_LOGIN_SECRET: e2e-test-secret-ci-only # Long TTL: the headless desktop binary holds one token for the whole # suite and cannot silently refresh; 15m expires mid-run on macOS. @@ -378,12 +317,8 @@ jobs: DB_DATABASE: cipherbox_test JWT_SECRET: desktop-e2e-jwt-secret-key CORS_ALLOWED_ORIGINS: http://localhost:5173,http://localhost:1420 - IPFS_PROVIDER: local - IPFS_LOCAL_API_URL: http://localhost:5001 - IPFS_LOCAL_GATEWAY_URL: http://localhost:8080 - DELEGATED_ROUTING_URL: http://localhost:3001 - REDIS_HOST: localhost - REDIS_PORT: 6379 + KUBO_API_URL: http://localhost:5001 + ROUTING_V1_URL: http://localhost:3001 TEST_LOGIN_SECRET: e2e-test-secret-ci-only IDENTITY_JWT_PRIVATE_KEY: ${{ secrets.IDENTITY_JWT_PRIVATE_KEY }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7363b7448..ff9e5b99c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ # Contributing to CipherBox -See [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md) for prerequisites and first-run instructions, -and [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for local development setup. +See the "Getting started" section of the root [README.md](README.md) for prerequisites, the local +stack, and first-run instructions. ## Branch Conventions diff --git a/README.md b/README.md index aae72edf1..dc4b44f76 100644 --- a/README.md +++ b/README.md @@ -109,27 +109,86 @@ cipher-box/ Prerequisites: Node.js 22+, pnpm 10+, Docker, and the Rust toolchain (pinned by `rust-toolchain.toml`). -```bash -# 1. Start infrastructure services -docker compose -f docker/docker-compose.yml up -d +Both services read a `.env` copied from a checked-in template. Those templates are the +one place the local stack's configuration is written down; this page does not repeat +their contents. + +### 1. Start the infrastructure -# 2. Install dependencies +```bash +docker compose -f docker/docker-compose.yml up -d --wait --wait-timeout 180 pnpm install +``` + +That brings up Postgres (5432), Kubo (5001 RPC, 8080 gateway), someguy (8190), and the +mock record store (3001). Kubo's RPC is an unauthenticated admin API and the dev compose +binds it to all interfaces, so run this stack on a network you trust. + +`--wait` holds until every service's healthcheck passes and exits non-zero if one does +not within the timeout, so the migration below cannot race a Postgres still starting. -# 3. Copy environment files +### 2. Configure and start the API + +```bash cp apps/api/.env.example apps/api/.env + +pnpm --filter @cipherbox/api migration:run +pnpm --filter @cipherbox/api dev +``` + +The template's defaults match the compose stack, so it runs as copied. Both the server +and the migration CLI read `apps/api/.env` from the package directory, which +`pnpm --filter` sets as the working directory. + +Its two secrets are throwaway values for a loopback stack — never reuse them in a +deployed environment. `JWT_SECRET` signs access tokens, and anyone holding +`TEST_LOGIN_SECRET` can mint a session for any account outside production. + +### 3. Build and serve the web app + +In a second shell: + +```bash cp apps/web/.env.example apps/web/.env -# 4. Start API and web app -pnpm dev +pnpm --filter @cipherbox/web dev ``` -- API: +- API: (OpenAPI at `/api-docs`) - Web: -Note that during the rewrite this boots the v2 skeleton (a stub API and web shell); the -legacy [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) still describes the v1 setup and is being -rewritten during the build. +Vite reads `.env` at build time, so rebuild after editing it. The template leaves +`VITE_READ_ACCELERATOR_URL` commented out on purpose: dormant is the content gateway's +fail-closed state, and a blank value must land there rather than configuring a gateway +whose every request fails. + +### Which record store the local stack uses + +Compose starts two `/routing/v1` backends, and a local stack should use +**`mock-ipns-routing` on port 3001** — what both templates ship, as `ROUTING_V1_URL` +(API republisher) and `VITE_ROUTING_ENDPOINTS` (web client). It is hermetic and +in-memory, so a record published locally resolves immediately and deterministically, and +no test vault's IPNS names reach the public network. CI and the web-e2e suite make the +same choice. + +`someguy` on 8190 participates in the real accelerated DHT. It is there for staging +parity and for deliberately testing public-network propagation; point the two variables +above at `http://localhost:8190` only when that is what you are testing. Both must name +the same backend, or the republisher re-PUTs into a store the client never reads. + +### What this stack can demonstrate today + +The API's write path is live end to end: authenticate and `POST /content/upload` +returns 201 with bytes pinned in the local Kubo. + +Interactive login through the web UI needs `VITE_WEB3AUTH_CLIENT_ID` and +`VITE_WEB3AUTH_VERIFIER`, which a clean checkout does not carry — the UI boots and +renders without them, but a Core Kit session cannot be created. The suites that need an +authenticated session use the build-time introspection hook instead; see +[`tests/web-e2e/README.md`](tests/web-e2e/README.md). + +A first folder create does not yet publish, because nothing provisions a fresh account's +first vault pointer, so its writes are accepted, rendered pending, and reach no endpoint. ## Security model diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 000000000..aaa74c4c5 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,38 @@ +# CipherBox API — local development template. +# +# Copy to apps/api/.env; both the server and the migration CLI read it from +# the package directory, which `pnpm --filter @cipherbox/api ...` sets. +# Every name below is read by the code — docs/CONFIGURATION.md catalogues the +# optional knobs this template leaves out. +# +# LOCAL ONLY. The two secrets here are throwaway values for a loopback stack. +# Never reuse them in a deployed environment. + +NODE_ENV=development +PORT=3000 +CORS_ALLOWED_ORIGINS=http://localhost:5173 + +# Postgres, matching docker/docker-compose.yml's defaults. +DB_HOST=localhost +DB_PORT=5432 +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE=cipherbox + +# Signs access tokens. A deployed API must set its own; the code refuses to +# fall back outside development and test. +JWT_SECRET=local-dev-jwt-secret + +# Enables POST /auth/test-login. Anyone holding this value can mint a session +# for any account outside production, where the route is hard-blocked. +TEST_LOGIN_SECRET=local-dev-test-secret + +# Kubo RPC for the hosted pin store. Unset, every hosted write answers 503 — +# uploads and folder creates alike, since a record's head block goes through +# the same endpoint. +KUBO_API_URL=http://localhost:5001 + +# The /routing/v1 endpoint the republisher resolves and re-PUTs through. +# 3001 is the compose stack's hermetic record store; someguy on 8190 is the +# real-DHT alternative. Must match the web app's VITE_ROUTING_ENDPOINTS. +ROUTING_V1_URL=http://localhost:3001 diff --git a/apps/api/src/content/content.module.ts b/apps/api/src/content/content.module.ts index 9f01720de..2fcb7716d 100644 --- a/apps/api/src/content/content.module.ts +++ b/apps/api/src/content/content.module.ts @@ -6,7 +6,7 @@ import { buildJwtOptions } from '../auth/auth.module'; import { User } from '../auth/entities/user.entity'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { PinnedCid } from '../registry/entities/pinned-cid.entity'; -import { KuboPinStore, PinStore } from '../registry/pin-store'; +import { RegistryModule } from '../registry/registry.module'; import { ContentController } from './content.controller'; import { ContentService } from './content.service'; @@ -19,6 +19,7 @@ import { ContentService } from './content.service'; @Module({ imports: [ TypeOrmModule.forFeature([PinnedCid, User]), + RegistryModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], @@ -26,6 +27,6 @@ import { ContentService } from './content.service'; }), ], controllers: [ContentController], - providers: [ContentService, JwtAuthGuard, { provide: PinStore, useClass: KuboPinStore }], + providers: [ContentService, JwtAuthGuard], }) export class ContentModule {} diff --git a/apps/api/src/registry/pin-store.test.ts b/apps/api/src/registry/pin-store.test.ts index d9b4df5a8..fae60c102 100644 --- a/apps/api/src/registry/pin-store.test.ts +++ b/apps/api/src/registry/pin-store.test.ts @@ -1,5 +1,5 @@ -import { ServiceUnavailableException } from '@nestjs/common'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Logger, ServiceUnavailableException } from '@nestjs/common'; +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'; import { fakeConfig } from '../testing/fakes'; import { KuboPinStore, PinCidMismatchError } from './pin-store'; @@ -86,3 +86,26 @@ describe('KuboPinStore.pin', () => { expect(calls).toEqual([]); }); }); + +describe('KuboPinStore configuration report', () => { + let errorSpy: MockInstance; + + beforeEach(() => { + errorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('names the unset variable and its consequence at construction', () => { + store(''); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('KUBO_API_URL')); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('503')); + }); + + it('stays silent when Kubo is configured', () => { + store(); + expect(errorSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/api/src/registry/pin-store.ts b/apps/api/src/registry/pin-store.ts index ff4f1a253..b6a97a39f 100644 --- a/apps/api/src/registry/pin-store.ts +++ b/apps/api/src/registry/pin-store.ts @@ -72,6 +72,13 @@ export class KuboPinStore extends PinStore { super(); const raw = configService.get('KUBO_API_URL'); this.apiUrl = raw && raw.trim() ? raw.replace(/\/+$/, '') : undefined; + if (!this.apiUrl) { + // At boot, not per request: otherwise a misconfigured deploy only learns + // this under load. + this.logger.error( + 'KUBO_API_URL is unset; hosted uploads will be refused with 503 and unpins will no-op' + ); + } } override async pin(cid: string, bytes: Uint8Array): Promise { diff --git a/apps/api/src/registry/registry.module.ts b/apps/api/src/registry/registry.module.ts index 1baf86afd..cff098986 100644 --- a/apps/api/src/registry/registry.module.ts +++ b/apps/api/src/registry/registry.module.ts @@ -36,5 +36,8 @@ import { RegistryService } from './services/registry.service'; JwtAuthGuard, { provide: PinStore, useClass: KuboPinStore }, ], + // The content slice binds the same instance rather than constructing a second + // one, so an unconfigured store is reported once at boot. + exports: [PinStore], }) export class RegistryModule {} diff --git a/apps/api/src/republisher/record-transport.test.ts b/apps/api/src/republisher/record-transport.test.ts index d20d95905..fb95cb827 100644 --- a/apps/api/src/republisher/record-transport.test.ts +++ b/apps/api/src/republisher/record-transport.test.ts @@ -1,4 +1,5 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { Logger } from '@nestjs/common'; +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'; import { fakeConfig } from '../testing/fakes'; import { RoutingV1RecordTransport } from './record-transport'; @@ -55,10 +56,8 @@ function fakeResponse(opts: { return { response, read, readerCancel, bodyCancel }; } -function transport(): RoutingV1RecordTransport { - return new RoutingV1RecordTransport( - fakeConfig({ ROUTING_V1_URL: 'https://routing.test' }).service - ); +function transport(routingUrl = 'https://routing.test'): RoutingV1RecordTransport { + return new RoutingV1RecordTransport(fakeConfig({ ROUTING_V1_URL: routingUrl }).service); } describe('RoutingV1RecordTransport response-size cap', () => { @@ -165,3 +164,29 @@ describe('RoutingV1RecordTransport response-size cap', () => { expect(read.mock.calls.length).toBeLessThan(16); }); }); + +describe('RoutingV1RecordTransport configuration report', () => { + let errorSpy: MockInstance; + + beforeEach(() => { + errorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('names the unset variable and its consequence at construction', () => { + transport(''); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('ROUTING_V1_URL')); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('the republisher walk will not run') + ); + }); + + it('stays silent when the routing endpoint is configured', () => { + transport(); + expect(errorSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/api/src/republisher/record-transport.ts b/apps/api/src/republisher/record-transport.ts index 14c59ece0..c20ce9373 100644 --- a/apps/api/src/republisher/record-transport.ts +++ b/apps/api/src/republisher/record-transport.ts @@ -59,6 +59,11 @@ export class RoutingV1RecordTransport extends RecordTransport { this.baseUrl = raw && raw.trim() ? raw.replace(/\/+$/, '') : undefined; const timeout = Number(configService.get('ROUTING_V1_TIMEOUT_MS')); this.timeoutMs = Number.isInteger(timeout) && timeout > 0 ? timeout : DEFAULT_TIMEOUT_MS; + if (!this.baseUrl) { + // The walk returns before it can alert, so boot is the only place this + // surfaces before names start expiring. + this.logger.error('ROUTING_V1_URL is unset; the republisher walk will not run'); + } } override get configured(): boolean { diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 000000000..0f0fc2076 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,28 @@ +# CipherBox web app — local development template. +# +# Copy to apps/web/.env. Vite reads it at BUILD time, so rebuild after editing. +# Every name below is read by the code; the commented ones are genuinely +# optional and are left unset on purpose. + +VITE_API_URL=http://localhost:3000 +VITE_ENVIRONMENT=local + +# Where the engine resolves and publishes records. Must name the same backend +# as the API's ROUTING_V1_URL, or the republisher re-PUTs into a store this +# client never reads. Unset, it defaults to the public delegated-ipfs.dev, +# which will not see records this stack publishes. +VITE_ROUTING_ENDPOINTS=http://localhost:3001 + +# Optional content-read accelerator, deliberately left unset: dormant is the +# fail-closed state, and a blank value must land there rather than configuring +# a gateway whose every request fails. Uncomment to point it at the compose +# Kubo gateway — but leave it absent rather than blank if you do not want it. +# VITE_READ_ACCELERATOR_URL=http://localhost:8080 + +# Optional comma-separated public gateways the engine may read from. +# VITE_PUBLIC_GATEWAYS= + +# Web3Auth Core Kit. Interactive login needs both. A clean checkout carries +# neither, so the UI renders but cannot create a session until they are set. +# VITE_WEB3AUTH_CLIENT_ID= +# VITE_WEB3AUTH_VERIFIER= diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ca24b8d1f..59b10bf0a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -394,4 +394,4 @@ Detailed specifications are maintained in separate documents: discipline, naming conventions, do-and-don't rules - [CAPACITY.md](CAPACITY.md) — storage limits, quota accounting, capacity planning - [VAULT_EXPORT_FORMAT.md](VAULT_EXPORT_FORMAT.md) — vault export/import format spec -- [DEVELOPMENT.md](DEVELOPMENT.md) — local dev setup, environment variables, workflow +- [../README.md](../README.md) — local dev setup: the one recipe that boots the stack diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a13e3ff38..0c0b53836 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2,15 +2,15 @@ # Configuration Reference +> **v1 document — partially superseded.** This catalogue was written against the v1 stack as of the freeze (`v1-freeze`, branch `v1`) and still names variables the v2 code no longer reads. The code is authoritative for which names are live; entries here are a starting point, not a contract. For the local stack itself, see the "Getting started" section of the root [`README.md`](../README.md). + Environment variables and configuration files for all CipherBox monorepo applications. -For local development setup instructions, see [DEVELOPMENT.md](DEVELOPMENT.md). ## Table of Contents - [API (`apps/api`)](#api-appsapi) - [Web (`apps/web`)](#web-appsweb) - [Desktop (`apps/desktop`)](#desktop-appsdesktop) -- [TEE Worker (`apps/tee-worker`)](#tee-worker-appstee-worker) - [Docker Compose (local dev)](#docker-compose-local-dev) - [Docker Compose (staging)](#docker-compose-staging) - [Observability (staging)](#observability-staging) @@ -20,7 +20,8 @@ For local development setup instructions, see [DEVELOPMENT.md](DEVELOPMENT.md). ## API (`apps/api`) NestJS server. Configuration is loaded via `@nestjs/config` (`ConfigModule.forRoot`) and read -from `.env` at startup. Copy `apps/api/.env.example` to `apps/api/.env` before first run. +from `.env` at startup. Copy `apps/api/.env.example` to `apps/api/.env` before first run; that +template carries the local stack's values and is the one this catalogue extends. ### Database @@ -32,17 +33,6 @@ from `.env` at startup. Copy `apps/api/.env.example` to `apps/api/.env` before f | `DB_PASSWORD` | No | `postgres` | PostgreSQL password | | `DB_DATABASE` | No | `cipherbox` | PostgreSQL database name | -### Redis - -| Variable | Required | Default | Description | -| :--------------- | :------- | :---------- | :-------------------------------------------------- | -| `REDIS_HOST` | No | `localhost` | Redis host | -| `REDIS_PORT` | No | `6379` | Redis port | -| `REDIS_PASSWORD` | No | — | Redis password (omit for password-less connections) | - -The local dev `docker/docker-compose.yml` maps the Redis container port to `6380` on the host, -so set `REDIS_PORT=6380` when using the local stack. - ### Auth | Variable | Required | Default | Description | @@ -58,24 +48,15 @@ so set `REDIS_PORT=6380` when using the local stack. ### IPFS -| Variable | Required | Default | Description | -| :----------------------- | :------- | :---------------------- | :-------------------------------------------------------------------------- | -| `IPFS_LOCAL_API_URL` | No | `http://localhost:5001` | Kubo RPC API endpoint. The API relays all IPFS operations through this URL. | -| `IPFS_LOCAL_GATEWAY_URL` | No | `http://localhost:8080` | IPFS HTTP gateway for content retrieval. | +| Variable | Required | Default | Description | +| :------------- | :------- | :------ | :-------------------------------------------------------------------------------------- | +| `KUBO_API_URL` | No | — | Kubo RPC endpoint for the hosted pin store. Unset, hosted uploads are refused with 503. | ### IPNS / Delegated Routing -| Variable | Required | Default | Description | -| :------------------------------- | :------- | :--------------------------- | :--------------------------------------------------------------- | -| `DELEGATED_ROUTING_URL` | No | `https://delegated-ipfs.dev` | Primary HTTP delegated routing backend for IPNS publish/resolve. | -| `DELEGATED_ROUTING_FALLBACK_URL` | No | — | Optional secondary backend. Used if the primary request fails. | - -### TEE Integration - -| Variable | Required | Default | Description | -| :------------------ | :------- | :---------------------- | :------------------------------------------------------------------------- | -| `TEE_WORKER_URL` | No | `http://localhost:3001` | URL of the TEE worker service. The API forwards IPNS republish jobs here. | -| `TEE_WORKER_SECRET` | No | `""` (empty) | Shared secret sent as `Authorization: Bearer` when calling the TEE worker. | +| Variable | Required | Default | Description | +| :--------------- | :------- | :------ | :------------------------------------------------------------------------------------------------------- | +| `ROUTING_V1_URL` | No | — | `/routing/v1` endpoint the republisher resolves and re-PUTs through. Unset, the republisher walk no-ops. | ### Rate Limiting @@ -98,89 +79,25 @@ so set `REDIS_PORT=6380` when using the local stack. Vite + React SPA. All configuration is injected as `VITE_*` environment variables at build time. Copy `apps/web/.env.example` to `apps/web/.env` before first run. -| Variable | Required | Default | Description | -| :------------------------ | :------- | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | -| `VITE_API_URL` | No | `http://localhost:3000` | Base URL of the CipherBox API. | -| `VITE_WEB3AUTH_CLIENT_ID` | **Yes** | — | Web3Auth project client ID for key derivation and authentication. | -| `VITE_GOOGLE_CLIENT_ID` | No | — | Google OAuth client ID for the Google Sign-In provider via Web3Auth. | -| `VITE_ENVIRONMENT` | No | `local` | Deployment environment label (`local`, `staging`, `production`). Used to show the staging banner in the UI. | -| `VITE_APP_VERSION` | No | (derived from crypto version) | Application version string injected at build time. Falls back to the internal crypto library version when absent. | -| `VITE_FARO_URL` | No | — | Grafana Faro collector endpoint. When absent, frontend observability is disabled. | +| Variable | Required | Default | Description | +| :------------------------ | :------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `VITE_API_URL` | No | `http://localhost:3000` | Base URL of the CipherBox API. | +| `VITE_WEB3AUTH_CLIENT_ID` | **Yes** | — | Web3Auth project client ID for key derivation and authentication. | +| `VITE_WEB3AUTH_VERIFIER` | **Yes** | — | Web3Auth Core Kit verifier name. Login needs this and the client ID; missing either refuses the session. | +| `VITE_GOOGLE_CLIENT_ID` | No | — | Google OAuth client ID for the Google Sign-In provider via Web3Auth. | +| `VITE_ENVIRONMENT` | No | `local` | Deployment environment label (`local`, `staging`, `production`). Used to show the staging banner in the UI. | +| `VITE_APP_VERSION` | No | (derived from crypto version) | Application version string injected at build time. Falls back to the internal crypto library version when absent. | +| `VITE_FARO_URL` | No | — | Grafana Faro collector endpoint. When absent, frontend observability is disabled. | --- ## Desktop (`apps/desktop`) -Tauri + Vite + React application. Uses the same `VITE_*` convention as the web app. -Copy `apps/desktop/.env.example` to `apps/desktop/.env` before first run. - -### Build-time (Vite) variables - -| Variable | Required | Default | Description | -| :------------------------ | :------- | :---------------------- | :-------------------------------------------------------------------------------------------------- | -| `VITE_API_URL` | No | `http://localhost:3000` | Base URL of the CipherBox API. | -| `VITE_WEB3AUTH_CLIENT_ID` | **Yes** | — | Web3Auth project client ID. | -| `VITE_GOOGLE_CLIENT_ID` | No | — | Google OAuth client ID. | -| `VITE_ENVIRONMENT` | No | `local` | Deployment environment label. | -| `VITE_TEST_LOGIN_SECRET` | No | — | Enables the test-login path inside the desktop app for E2E testing. Never set in production builds. | - -### Runtime (Rust backend) variables - -The Tauri Rust backend loads `apps/desktop/.env` at startup and resolves the API base URL in -this order: - -1. Runtime env `CIPHERBOX_API_URL` (manual override) -2. Runtime env `VITE_API_URL` -3. Compile-time `VITE_API_URL` (baked into release builds by CI) -4. Fallback `http://localhost:3000` - -| Variable | Required | Default | Description | -| :------------------ | :------- | :------ | :------------------------------------------------------------------------------------------------------------------------------ | -| `CIPHERBOX_API_URL` | No | — | Runtime override for the API base URL used by the Rust backend (sync engine, FUSE mount). Takes precedence over `VITE_API_URL`. | - -### Tauri configuration (`apps/desktop/src-tauri/tauri.conf.json`) - -Static build-time configuration — not environment-variable driven. - -| Key | Value | Description | -| :---------------------------------- | :---------------------------- | :------------------------------------------- | -| `productName` | `CipherBox` | Application display name. | -| `identifier` | `com.cipherbox.desktop` | Bundle identifier used on macOS and Linux. | -| `plugins.updater.endpoints` | GitHub Releases `latest.json` | Auto-updater manifest URL. | -| `plugins.deep-link.desktop.schemes` | `cipherbox` | Custom URL scheme registered with the OS. | -| `build.devUrl` | `http://localhost:1420` | Vite dev server URL used during `tauri dev`. | - -The updater `pubkey` in `tauri.conf.json` is the Minisign public key used to verify update -artifacts — it is safe to commit and is not a secret. - ---- - -## TEE Worker (`apps/tee-worker`) - -Standalone Express server. Runs inside a Phala Cloud CVM in production and in a local -Docker container (simulator mode) in development and staging. - -Copy `apps/tee-worker/.env.example` to `apps/tee-worker/.env` for local development. - -| Variable | Required | Default | Description | -| :---------------------- | :------- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PORT` | No | `3001` | HTTP listen port. | -| `NODE_ENV` | No | — | Runtime environment. Setting `production` while `TEE_MODE=simulator` is blocked at startup. | -| `TEE_MODE` | No | `simulator` | Key derivation mode. `simulator` uses HKDF from a fixed seed (development/testing). `cvm` uses Phala dstack SDK for hardware-backed key derivation (production). | -| `CIPHERBOX_ENVIRONMENT` | No | — | Explicit environment label (`staging`, `production`). Used alongside `NODE_ENV` to enforce that `TEE_MODE=simulator` is never used in production. | -| `TEE_WORKER_SECRET` | **Yes** | — | Shared secret for Bearer token authentication on all protected routes. Must match the `TEE_WORKER_SECRET` set in the API. | -| `TEE_CURRENT_EPOCH` | No | `1` | The current `keyEpoch` number. The TEE worker exposes the `teePublicKey` for this epoch on `GET /public-key`. Used by the migration route to identify the active key epoch. | -| `IPFS_GATEWAY_URL` | No | `https://ipfs.io` | IPFS gateway URL used when fetching CIDs during CID migration operations. | - -### TEE mode semantics - -The `TEE_MODE` variable determines how epoch keypairs are derived: - -- **`simulator`** — HKDF-SHA256 derivation from a fixed seed. Deterministic across restarts. - Used for local development and staging. Never allowed when `NODE_ENV=production` or - `CIPHERBOX_ENVIRONMENT=production`. -- **`cvm`** — Phala dstack `DstackClient.getKey()` call. Hardware-backed, non-extractable. - Required for production Phala Cloud CVM deployments. +The v2 Tauri shell is a skeleton: it carries no TypeScript sources and reads no environment +variable, so there is no `apps/desktop/.env.example` to copy and nothing here to configure. +The tables that stood here described the v1 desktop app and named variables — `VITE_GOOGLE_CLIENT_ID`, +`VITE_TEST_LOGIN_SECRET`, `CIPHERBOX_API_URL` — that no code reads. They are removed rather +than corrected; `blueprint/desktop.md` is normative for what the shell will need. --- @@ -188,7 +105,7 @@ The `TEE_MODE` variable determines how epoch keypairs are derived: File: `docker/docker-compose.yml` -Starts PostgreSQL, IPFS (Kubo), Redis, Someguy (delegated routing), and a mock IPNS routing +Starts PostgreSQL, IPFS (Kubo), Someguy (delegated routing), and a mock IPNS routing server for local development. Environment variables can be overridden with a `.env` file in the `docker/` directory or by setting them in the shell before running `docker compose up`. @@ -198,7 +115,6 @@ server for local development. Environment variables can be overridden with a `.e | `DB_PASSWORD` | `postgres` | PostgreSQL superuser password. | | `DB_DATABASE` | `cipherbox` | Database name to create. | | `DB_PORT` | `5432` | Host port mapped to PostgreSQL 5432 inside the container. | -| `REDIS_PORT` | `6380` | Host port mapped to Redis 6379 inside the container. | Service ports exposed to the host: @@ -207,7 +123,6 @@ Service ports exposed to the host: | PostgreSQL | `5432` (configurable) | | | IPFS API | `5001` | Bound to all interfaces in dev | | IPFS Gateway | `8080` | Bound to all interfaces in dev | -| Redis | `6380` (configurable) | | | Someguy (delegated routing) | `8190` (HTTP), `4004` (libp2p) | | | Mock IPNS routing | `3001` (localhost only) | | @@ -217,8 +132,8 @@ Service ports exposed to the host: File: `docker/docker-compose.staging.yml` -Deploys the full stack including the API, IPFS, Redis, PostgreSQL, TEE worker, Someguy, Caddy -reverse proxy, and Grafana Alloy for log/metrics forwarding. +Deploys the full stack including the API, IPFS, PostgreSQL, Someguy, Caddy reverse proxy, and +Grafana Alloy for log/metrics forwarding. The API service reads its environment from `.env.staging` (passed via `env_file`). @@ -227,33 +142,15 @@ The API service reads its environment from `.env.staging` (passed via `env_file` These variables must be set in `.env.staging` (API) or directly in the Docker Compose environment block (infrastructure services): -| Variable | Service | Notes | -| :----------------------- | :--------------- | :------------------------------------------------------------- | -| `DB_USERNAME` | postgres | Defaults to `cipherbox` | -| `DB_PASSWORD` | postgres | **Required** — no default in staging | -| `DB_DATABASE` | postgres | Defaults to `cipherbox_staging` | -| `JWT_SECRET` | api | **Required** — any strong random string | -| `REDIS_PASSWORD` | redis / api | **Required** — staging Redis runs with `requirepass` | -| `TEE_WORKER_SECRET` | tee-worker / api | Must match between both services | -| `CORS_ALLOWED_ORIGINS` | api | Set to the deployed web app origin(s) | -| `IPFS_LOCAL_API_URL` | api | Typically `http://ipfs:5001` inside compose network | -| `IPFS_LOCAL_GATEWAY_URL` | api | Typically `http://ipfs:8080` inside compose network | -| `DELEGATED_ROUTING_URL` | api | Set to `http://someguy:8190` to use the local Someguy instance | -| `TEE_WORKER_URL` | api | Typically `http://tee-worker:3001` inside compose network | - -### Phala Cloud CVM (production TEE worker) - -File: `apps/tee-worker/docker-compose.phala.yml` - -Used for production TEE deployments on Phala Cloud. Key constraint: always **update** the -existing CVM using the same `--name` value — never delete and recreate. Recreating changes the -`app_id`, which invalidates all epoch-derived keys. - -| Variable | Source | Notes | -| :------------------------ | :--------------- | :---------------------------------------- | -| `TEE_WORKER_SECRET` | host environment | Injected at deploy time | -| `GITHUB_REPOSITORY_OWNER` | host environment | Used to resolve the container image path | -| `TAG` | host environment | Image tag to deploy; defaults to `latest` | +| Variable | Service | Notes | +| :--------------------- | :------- | :------------------------------------------------------ | +| `DB_USERNAME` | postgres | Defaults to `cipherbox` | +| `DB_PASSWORD` | postgres | **Required** — no default in staging | +| `DB_DATABASE` | postgres | Defaults to `cipherbox_staging` | +| `JWT_SECRET` | api | **Required** — any strong random string | +| `CORS_ALLOWED_ORIGINS` | api | Set to the deployed web app origin(s) | +| `KUBO_API_URL` | api | Typically `http://ipfs:5001` inside compose network | +| `ROUTING_V1_URL` | api | `http://someguy:8190` to use the local Someguy instance | --- diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2dda8d77a..783a89eaa 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,217 +1,5 @@ # Development Guide -## Prerequisites +> **v1 document — superseded.** This described the v1 local development setup as of the freeze (`v1-freeze`, branch `v1`). The current local-stack recipe lives in exactly one place: the "Getting started" section of the root [`README.md`](../README.md). -- **Node.js** 20+ -- **pnpm** 9+ -- **Docker** (for PostgreSQL, IPFS, Redis) -- **Rust** toolchain (desktop app only) - -## Infrastructure - -Start the required services: - -```bash -docker compose -f docker/docker-compose.yml up -d -``` - -This starts the `cipherbox-infrastructure` compose project (containers are named `cipherbox-`): - -| Service | Image | Host Port(s) | Purpose | -| :------------------ | :------------------------------------ | :----------------------------------------------- | :--------------------------------------- | -| `postgres` | `postgres:16-alpine` | 5432 (`DB_PORT`) | Database | -| `ipfs` | `ipfs/kubo:v0.42.0` | 5001 (API), 8080 (gateway), 4001 tcp/udp (swarm) | Decentralized storage (Kubo) | -| `redis` | `redis:7-alpine` | 6380 (`REDIS_PORT`) → container 6379 | BullMQ job queue | -| `someguy` | `ghcr.io/ipfs/someguy:v0.11.1` | 8190 (routing API), 4004 tcp/udp (libp2p swarm) | Delegated IPFS routing (accelerated DHT) | -| `mock-ipns-routing` | built from `tools/mock-ipns-routing/` | 3001 (loopback only) | Local IPNS resolution for dev/E2E | - -Notes: - -- The IPFS node runs with the `server,pebbleds` datastore profile. If you have an `ipfs_data` volume created before the pebbleds switch, recreate it first: `docker compose -f docker/docker-compose.yml down -v --remove-orphans`. -- The `ipfs` container is capped at 3 GB memory / 1.5 CPU; the `someguy` container is capped at 2 GB memory / 1 CPU. -- The staging stack runs a different set of containers (adds `api`, `tee-worker`, `caddy`, `alloy`; drops `mock-ipns-routing`) — see [DEPLOYMENT.md](DEPLOYMENT.md). -- **Strict IPNS verification cutover (Phase 60):** If your local database was created before the strict-verify cutover landed, it contains `folder_ipns` records with `sequence_number = 0` (embedded-0 records) that the API now rejects. These records cause fail-closed errors on any IPNS publish or resolve. Wipe your local database and let the API recreate it via migrations before running the strict build: `dropdb cipherbox && createdb cipherbox && pnpm --filter @cipherbox/api dev`. See [docs/DATABASE_EVOLUTION_PROTOCOL.md](DATABASE_EVOLUTION_PROTOCOL.md) §7 (Environment Behavior Matrix) for the full reset procedure. - -## Environment - -Copy the example env files: - -```bash -cp apps/api/.env.example apps/api/.env -cp apps/web/.env.example apps/web/.env -``` - -### API (`apps/api/.env`) - -Key variables: - -| Variable | Default | Notes | -| :--------------------- | :---------------------- | :-------------------------------------- | -| `DB_HOST` | `localhost` | PostgreSQL host | -| `DB_PORT` | `5432` | PostgreSQL port | -| `JWT_SECRET` | — | Required, any random string | -| `REDIS_HOST` | `localhost` | Redis host | -| `REDIS_PORT` | `6380` | Redis port (mapped from container 6379) | -| `CORS_ALLOWED_ORIGINS` | `http://localhost:5173` | Frontend origin | - -### Web (`apps/web/.env`) - -| Variable | Default | Notes | -| :------------------------ | :------------------------- | :------------------ | -| `VITE_WEB3AUTH_CLIENT_ID` | Provided in `.env.example` | Web3Auth project ID | -| `VITE_API_URL` | `http://localhost:3000` | API endpoint | - -## Running the Web App - -```bash -# Install dependencies (first time) -pnpm install - -# Start API + web concurrently -pnpm dev -``` - -- API: -- Web: - -Or run individually: - -```bash -pnpm --filter @cipherbox/api dev # API only -pnpm --filter @cipherbox/web dev # Web only -``` - -## Running the Desktop App - -### Additional prerequisites - -- **macOS:** [FUSE-T](https://www.fuse-t.org/) (`brew install macos-fuse-t/homebrew-cask/fuse-t`) -- **Windows:** [WinFSP](https://winfsp.dev/) -- **Linux:** `libfuse3-dev` (or equivalent) - -```bash -cp apps/desktop/.env.example apps/desktop/.env -pnpm --filter @cipherbox/desktop dev -``` - -The desktop app defaults to the staging API. For local development, update `apps/desktop/.env`: - -```bash -VITE_API_URL=http://localhost:3000 -VITE_ENVIRONMENT=local -``` - -The Rust backend also needs the local API URL. Either set it in your shell or prefix the dev command: - -```bash -CIPHERBOX_API_URL=http://localhost:3000 pnpm --filter @cipherbox/desktop dev -``` - -See [apps/desktop/CLAUDE.md](../apps/desktop/CLAUDE.md) for FUSE architecture details and dev-key mode. - -## Testing - -### Unit tests - -```bash -# Run all unit tests (excludes E2E) -pnpm --filter @cipherbox/api test -pnpm --filter @cipherbox/web test -pnpm --filter @cipherbox/crypto test -``` - -> **Note:** `pnpm test` runs tests across all workspaces including E2E — use the filtered commands above for unit tests only. - -### Test architecture and CI coverage (the deliberate split) - -The repo follows a deliberate testing split, and CI enforces it accordingly (decision D-06): - -- **Reusable / business logic → `packages/sdk` (Vitest, CI-gated).** Any logic worth unit-testing is hoisted out of `apps/web` into `packages/sdk` (or another package), where it is covered by Vitest. These suites run in the blocking CI `Test` job (`.github/workflows/ci.yml`), alongside `crypto`, `core`, `sdk-core`, `sdk`, `api-client`, and `api`. -- **UI behavior → Playwright web-e2e.** User-facing flows are covered by the Playwright web-e2e suite (`pnpm test:web-e2e`), which is dispatch/main-push gated rather than a per-PR blocking unit job. -- **`apps/web` Vitest is intentionally NOT in a blocking CI unit-test job.** A residual `apps/web` `*.test.ts` suite exists (10 files / 67 tests) and must stay green, but it is deliberately excluded from the blocking CI `Test` job. This is a decision, not an accidental gap: gating CI on `apps/web` Vitest would invite UI-coupled unit tests, which the split above is designed to prevent. Logic that deserves a unit test belongs in `packages/sdk`, not in a web-local test. - -Two caveats when working with the residual `apps/web` suite: - -- **`.spec.ts` is silently skipped.** The `apps/web` Vitest `include` glob matches `*.test.ts` only, so any `*.spec.ts` file is silently excluded — never rely on a `.spec.ts` under `apps/web` being executed. -- **Build the cross-package dist chain first.** Running the web suite locally requires the workspace dist to be built, or workspace-package resolution fails. Build the chain, then run the suite: - - ```bash - pnpm --filter @cipherbox/crypto build \ - && pnpm --filter @cipherbox/core build \ - && pnpm --filter @cipherbox/api-client build \ - && pnpm --filter @cipherbox/sdk-core build \ - && pnpm --filter @cipherbox/sdk build \ - && cd apps/web && pnpm vitest run - ``` - -If a residual `apps/web` test genuinely rots, relocate its logic to `packages/sdk` (Vitest) or remove the dead test — do not add new `apps/web` unit tests, and do not paper over a real failure by skipping it. - -### Strict IPNS verification — wipe local DB first - -The strict fail-closed IPNS verification cutover (Phase 60 / HARD-11) rejects any IPNS record that embeds sequence `0`. A local dev database created before the cutover holds such "embedded-0" records, so a pre-existing vault or folder will fail strict verification and fail to resolve. Before running the strict build against an existing local DB, wipe it per [`DATABASE_EVOLUTION_PROTOCOL.md`](./DATABASE_EVOLUTION_PROTOCOL.md) (§reset) and log in again — the vault self-bootstraps fresh strict-verified records. Because all IPNS keys are deterministically derived from the Web3Auth key, the wipe is non-destructive to identity. - -### E2E tests (Playwright) - -Playwright auto-starts API + web via `webServer` config (requires infra services: Postgres, IPFS, Redis): - -```bash -pnpm test:web-e2e -``` - -Headed mode (shows browser): - -```bash -pnpm test:web-e2e:headed -``` - -### Desktop E2E - -```bash -cd tests/desktop-e2e -pnpm exec playwright test -``` - -## API Client Generation - -After modifying API endpoints, DTOs, or controllers, regenerate the typed client to keep the web app in sync: - -```bash -pnpm api:generate -``` - -This generates the OpenAPI spec from the API, creates the typed client at `packages/api-client/`, and runs lint fixes. Always commit the regenerated files with your API changes. - -## Code Quality - -- **Linting:** `pnpm lint` (ESLint) / `pnpm lint:fix` -- **Markdown:** `pnpm lint:md` / `pnpm lint:md:fix` -- **Type checking:** `pnpm typecheck` -- **Formatting:** Prettier (runs via lint-staged on commit) -- **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) enforced by commitlint (`feat:`, `fix:`, `docs:`, etc.) - -## Running the TEE Worker (Simulator Mode) - -The TEE worker (`apps/tee-worker`) republishes IPNS records. In production it runs inside a Phala Cloud CVM; locally it runs in **simulator mode**, which uses a deterministic HKDF-SHA256 seed instead of hardware-backed key derivation. - -Set the required environment variables and start the worker: - -```bash -TEE_MODE=simulator TEE_WORKER_SECRET=dev-secret pnpm --filter cipherbox-tee-worker dev -``` - -The worker listens on port `3001` by default. Note that the `mock-ipns-routing` container also binds `127.0.0.1:3001` — when running the worker alongside the local Docker infrastructure, pick a free port (e.g. `PORT=3002`) and point `TEE_WORKER_URL` at it. The API authenticates to the TEE worker using a shared `Bearer` token — set the same value in `apps/api/.env`: - -```bash -TEE_WORKER_SECRET=dev-secret -TEE_WORKER_URL=http://localhost:3001 -``` - -Key variables: - -| Variable | Required | Notes | -| :------------------ | :------- | :---------------------------------------------- | -| `TEE_MODE` | Yes | `simulator` (local) or `cvm` (Phala Cloud prod) | -| `TEE_WORKER_SECRET` | Yes | Shared secret for Bearer token auth | -| `PORT` | No | Defaults to `3001` | - -`TEE_MODE=simulator` is blocked at runtime if `NODE_ENV=production` or `CIPHERBOX_ENVIRONMENT=production` to prevent accidental use of the fixed seed in production. +For the normative v2 architecture, read the `blueprint/` corpus and `CONTEXT.md`. The v1 spec corpus, ADRs, and design-decision history live in [FSM1/cipher-box-next](https://github.com/FSM1/cipher-box-next). diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 315bf0ad1..d7cde06f7 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -2,183 +2,6 @@ # Getting Started with CipherBox -CipherBox is a privacy-first encrypted cloud storage system using IPFS/IPNS and Web3Auth. -This guide takes you from a fresh checkout to a running local stack. +> **v1 document — superseded.** This described the v1 first-run setup as of the freeze (`v1-freeze`, branch `v1`). The current local-stack recipe lives in exactly one place: the "Getting started" section of the root [`README.md`](../README.md). -## Prerequisites - -| Tool | Version | Notes | -| :------------- | :--------- | :--------------------------------------------- | -| Node.js | 20+ | Used by all JS/TS workspaces | -| pnpm | 10.33.0 | Declared in `packageManager` field | -| Docker | Any recent | Runs PostgreSQL, IPFS, Redis, and mock routing | -| Rust toolchain | stable | Desktop app, and the web app's engine WASM | - -No `.nvmrc` is present; use your system Node version manager to select Node 20+. - -`apps/web` compiles `crates/wasm` into the engine worker's artifact on every `dev`/`build`, so it also -needs the browser target and a `wasm-bindgen-cli` matching the `wasm-bindgen` version in `Cargo.lock`: - -```bash -rustup target add wasm32-unknown-unknown -cargo install wasm-bindgen-cli --version "$(grep -A1 '^name = "wasm-bindgen"$' Cargo.lock | grep '^version' | head -1 | sed 's/.*"\(.*\)".*/\1/')" --locked -``` - -For the desktop app, also install the platform FUSE driver: - -- **macOS:** `brew install macos-fuse-t/homebrew-cask/fuse-t` -- **Windows:** [WinFSP](https://winfsp.dev/) -- **Linux:** `libfuse3-dev` (or the equivalent for your distribution) - -## Installation - -```bash -git clone https://github.com/YOUR_ORG/cipher-box.git -cd cipher-box -pnpm install -``` - -## Local Infrastructure - -All required backing services are defined in `docker/docker-compose.yml`. Start them before -running any application: - -```bash -docker compose -f docker/docker-compose.yml up -d -``` - -This starts the following services: - -| Service | Port(s) | Purpose | -| :-------------------------- | :--------------------------------------- | :------------------------------------ | -| PostgreSQL 16 | 5432 | Primary database | -| IPFS (Kubo v0.40.0) | 5001 (API), 8080 (Gateway), 4001 (Swarm) | Decentralized file storage | -| Redis 7 | 6380 (host) → 6379 (container) | BullMQ job queue | -| Someguy (delegated routing) | 8190 (HTTP), 4004 (libp2p) | IPFS DHT routing | -| Mock IPNS routing | 3001 | Local IPNS resolution for dev and E2E | - -Wait for all containers to be healthy before starting the applications: - -```bash -docker compose -f docker/docker-compose.yml ps -``` - -## Environment Setup - -Copy the example environment files for each application you intend to run: - -```bash -cp apps/api/.env.example apps/api/.env -cp apps/web/.env.example apps/web/.env -``` - -At minimum, set `JWT_SECRET` in `apps/api/.env` — startup fails without it. The default values -for all other variables in the `.env.example` files match the local Docker Compose stack. - -For the full list of variables and their descriptions, see [CONFIGURATION.md](CONFIGURATION.md). - -### Redis port note - -The Docker Compose file maps the Redis container port to **6380** on the host. Ensure -`apps/api/.env` contains `REDIS_PORT=6380` (this is already set in `.env.example`). - -## Running the Web Stack - -Start the API and web app together: - -```bash -pnpm dev -``` - -This runs `@cipherbox/api` and `@cipherbox/web` concurrently via `concurrently`. - -Or run each individually: - -```bash -pnpm --filter @cipherbox/api dev -pnpm --filter @cipherbox/web dev -``` - -Default URLs: - -- API: `http://localhost:3000` -- Web: `http://localhost:5173` - -## Running the Desktop App - -The desktop app requires the Rust toolchain and a FUSE driver (see Prerequisites above). - -```bash -cp apps/desktop/.env.example apps/desktop/.env -pnpm --filter @cipherbox/desktop dev -``` - -The desktop app targets the **staging API by default**. To develop against your local API, -edit `apps/desktop/.env`: - -```env -VITE_API_URL=http://localhost:3000 -VITE_ENVIRONMENT=local -``` - -Also pass the API URL to the Rust backend: - -```bash -CIPHERBOX_API_URL=http://localhost:3000 pnpm --filter @cipherbox/desktop dev -``` - -## Running the TEE Worker - -The TEE worker handles IPNS republishing. It is not required for basic web app development. - -```bash -pnpm --filter cipherbox-tee-worker dev -``` - -## First-Use Walkthrough - -1. Start infrastructure: `docker compose -f docker/docker-compose.yml up -d` -2. Start the web stack: `pnpm dev` -3. Open `http://localhost:5173` in your browser -4. Log in using Web3Auth (social login or wallet) -5. On first login a new encrypted vault is created — you will be prompted to save your - recovery factor -6. Upload a file using the drag-and-drop interface or the upload button -7. The file is encrypted client-side and stored on IPFS; the metadata is published to IPNS - -## Common Setup Issues - -### `JWT_SECRET` is missing - -The API exits immediately at startup if `JWT_SECRET` is not set in `apps/api/.env`. Set it -to any non-empty string for local development. - -### Redis connection refused - -The Docker Compose stack maps Redis to port 6380, not the default 6379. Confirm -`REDIS_PORT=6380` in `apps/api/.env`. - -### IPFS container unhealthy - -The IPFS container has a 30-second start period. If `docker compose ps` shows it as unhealthy -immediately after starting, wait 30–60 seconds and check again. If it remains unhealthy: - -```bash -docker compose -f docker/docker-compose.yml logs ipfs -``` - -### pnpm version mismatch - -The `packageManager` field pins pnpm to `10.33.0`. If your global pnpm differs, enable -[Corepack](https://nodejs.org/api/corepack.html) to use the pinned version automatically: - -```bash -corepack enable -``` - -## Next Steps - -- [DEVELOPMENT.md](DEVELOPMENT.md) — build commands, code style, branch conventions, PR process -- [CONFIGURATION.md](CONFIGURATION.md) — full environment variable reference for all apps -- [ARCHITECTURE.md](ARCHITECTURE.md) — system architecture and component overview -- [TESTING.md](TESTING.md) — how to run unit tests and E2E tests +For the normative v2 architecture, read the `blueprint/` corpus and `CONTEXT.md`. The v1 spec corpus, ADRs, and design-decision history live in [FSM1/cipher-box-next](https://github.com/FSM1/cipher-box-next).