Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
18 changes: 6 additions & 12 deletions .github/workflows/desktop-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,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
KUBO_API_URL=http://localhost:5001
ROUTING_V1_URL=http://localhost:3001
REDIS_HOST=localhost
REDIS_PORT=6379
TEST_LOGIN_SECRET=e2e-test-secret-ci-only
Expand Down Expand Up @@ -337,10 +335,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
KUBO_API_URL: http://localhost:5001
ROUTING_V1_URL: http://localhost:3001
REDIS_HOST: localhost
REDIS_PORT: 6379
TEST_LOGIN_SECRET: e2e-test-secret-ci-only
Expand Down Expand Up @@ -378,10 +374,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
KUBO_API_URL: http://localhost:5001
ROUTING_V1_URL: http://localhost:3001
REDIS_HOST: localhost
REDIS_PORT: 6379
TEST_LOGIN_SECRET: e2e-test-secret-ci-only
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: 71 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,27 +109,88 @@ cipher-box/
Prerequisites: Node.js 22+, pnpm 10+, Docker, and the Rust toolchain (pinned by
`rust-toolchain.toml`).

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

```bash
# 1. Start infrastructure services
docker compose -f docker/docker-compose.yml up -d

# 2. Install dependencies
pnpm install
```

# 3. Copy environment files
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 for the
services to report healthy:

```bash
docker compose -f docker/docker-compose.yml ps
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

### 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 {}
5 changes: 5 additions & 0 deletions apps/api/src/republisher/record-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -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=
Comment thread
FSM1 marked this conversation as resolved.
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading