Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
77 changes: 6 additions & 71 deletions .github/workflows/desktop-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 }}

Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
81 changes: 70 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <http://localhost:3000>
- API: <http://localhost:3000> (OpenAPI at `/api-docs`)
- Web: <http://localhost:5173>

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

Expand Down
38 changes: 38 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions apps/api/src/content/content.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -19,13 +19,14 @@ import { ContentService } from './content.service';
@Module({
imports: [
TypeOrmModule.forFeature([PinnedCid, User]),
RegistryModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: buildJwtOptions,
}),
],
controllers: [ContentController],
providers: [ContentService, JwtAuthGuard, { provide: PinStore, useClass: KuboPinStore }],
providers: [ContentService, JwtAuthGuard],
})
export class ContentModule {}
27 changes: 25 additions & 2 deletions apps/api/src/registry/pin-store.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -86,3 +86,26 @@ describe('KuboPinStore.pin', () => {
expect(calls).toEqual([]);
});
});

describe('KuboPinStore configuration report', () => {
let errorSpy: MockInstance<Logger['error']>;

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();
});
});
7 changes: 7 additions & 0 deletions apps/api/src/registry/pin-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ export class KuboPinStore extends PinStore {
super();
const raw = configService.get<string>('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<void> {
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/registry/registry.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Loading