# Contributing Thanks for your interest in Secret. This guide covers setting up the project, what a change needs before it's merged, and where things go. Releases are described for maintainers in [docs/releasing.md](docs/releasing.md). ## Prerequisites - **Node.js 26 or later.** The code uses `node:sqlite` and `Error.isError`. Check with `node --version`; with nvm, run `nvm use 26`. - **pnpm 10**, through corepack, which picks up the version pinned in `package.json`. Node 25 and later don't ship corepack: install the version the Dockerfile uses, `npm install -g corepack@0.35.0 && corepack enable`. - **Docker** (optional), for the containerized workflow below. ## Getting started ```bash git clone https://github.com/largerio/secret.git cd secret pnpm install # The API reads its config from the shell environment (there is no dotenv # loader), so export a key before starting it: export SERVER_ENCRYPTION_KEY=$(openssl rand -base64 32) pnpm dev ``` This starts the API on `http://localhost:3000` and the web app on `http://localhost:5173`, whose dev server sends `/api` to the API. `PORT` moves the API, and the dev server follows it. The key must decode to exactly 32 bytes, or the server refuses to start. Keep the same key across restarts: the notes you created earlier can't be read with another one, and the server refuses to start rather than ignore it. ### With Docker The `Makefile` wraps `docker-compose.dev.yml`, needs no local Node install, and ships a working dev key: ```bash make dev # API + web with live reload make check # lint + typecheck + test make shell # a shell inside the dev container ``` ## Making a change 1. Create a branch from `main`. 2. Make your change, with tests. New behavior without tests will be asked for them. 3. Run the checks: ```bash pnpm lint # Biome lint + format pnpm typecheck # TypeScript strict pnpm test:coverage # the whole suite, with the 100% coverage gate CI enforces ``` 4. Update the docs that describe what you changed: - a change people will notice in the app: an entry under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md); - a change to the SDK or the command line: a changeset, with `pnpm changeset` ([npm packages](docs/releasing.md#npm-packages)); - a change to the note format, the payload, a read or upload endpoint, a limit a client must respect, or a new error code: [PROTOCOL.md](PROTOCOL.md), with new test vectors when the bytes change. 5. Commit with a clear message (see [Commit messages](#commit-messages)), and open a pull request. ## Code style We use [Biome](https://biomejs.dev/) for linting and formatting: - **Indentation**: tabs - **Line width**: 100 characters - **Quotes**: double quotes - **Semicolons**: always Run `pnpm lint:fix` to fix most issues. ### TypeScript Strict mode is on, with these additional flags: - `noUncheckedIndexedAccess`: indexing an array or object returns `T | undefined` - `exactOptionalPropertyTypes`: `undefined` must be explicit - `noNonNullAssertion`: no `!` assertions - `noExplicitAny`: no `any` type (enforced by Biome) Use `import type { ... }` for type-only imports. ## Project structure ``` apps/api/ Backend (Hono, Node.js 26, SQLite), which also serves the web app apps/web/ Frontend (SvelteKit, Svelte 5, Tailwind CSS 4), built as static files apps/e2e/ Playwright end-to-end tests (incl. the axe-core a11y gate) packages/sdk-js/ JS/TS SDK (SecretClient, encrypt/decrypt flows) packages/cli/ Command line (`secret`), built on the SDK packages/crypto/ Encryption library (libsodium, AES-256-GCM) packages/shared/ Shared types, Zod schemas, constants, crypto test vectors messages/ i18n translations (10 languages) ``` ### Where to put things - **New API endpoint**: `apps/api/src/routes/` - **New validation schema**: `packages/shared/src/validation.ts` - **New type**: `packages/shared/src/types.ts` (export it from `index.ts`) - **New constant**: `packages/shared/src/constants.ts` (export it from `index.ts`) - **New SDK feature**: `packages/sdk-js/src/` - **New crypto function**: `packages/crypto/src/` - **New UI component**: `apps/web/src/lib/components/` - **New translation key**: every file in `messages/` (see [i18n](#i18n)) - **Storage backend**: implement the `StorageBackend` interface in `apps/api/src/storage/` ## Testing We use [Vitest](https://vitest.dev/). Tests live in `__tests__/` directories next to the source. ```bash pnpm test # run once pnpm test:watch # watch mode pnpm test:coverage # with the coverage gate ``` ### Writing tests - **API routes**: use `app.request()` (Hono's test helper, no HTTP server) - **Database**: use real SQLite files, cleaned up in `beforeEach` - **Crypto**: call `await initSodium()` in `beforeAll` - **S3**: mock `@aws-sdk/client-s3` with class-based mocks (see `s3.test.ts`) ### End-to-end tests ```bash pnpm --filter @largerio/e2e exec playwright install --with-deps chromium # the first time pnpm test:e2e ``` The suite builds the web app and starts the API serving it, as the image does. CI runs it against the image instead, on S3 storage: `.github/scripts/e2e-image.sh ` starts both, then `E2E_BASE_URL=http://localhost:3000 pnpm test:e2e` targets them. With the web app built (`pnpm --filter @largerio/web build`), `scripts/verify-web-app.sh http://localhost:3000` then checks that the image serves that very build, as CI does, and as anyone can of an instance ([SECURITY.md](SECURITY.md#verifying-the-web-app)). The command line runs against the same image, installed with npm from its packed tarball and the SDK's, as a user installs it. Start the image with an API key of at least 32 characters in `E2E_API_KEY`, build the SDK and the CLI (`pnpm --filter @largerio/secret-sdk --filter @largerio/secret-cli build`), then run `.github/scripts/e2e-cli.sh http://localhost:3000` with the same `E2E_API_KEY`. CI runs it on Node 22, the oldest the CLI supports. ## Commit messages Follow [Conventional Commits](https://www.conventionalcommits.org/): ``` feat: add S3 storage backend fix: timing-safe delete token comparison test: add multipart upload endpoint tests docs: update README with S3 configuration refactor: extract storage abstraction layer ``` ## Security Secret is built around one promise: the server never sees what people send. Please keep in mind: - **Never log or expose plaintext content.** The server must stay zero-knowledge. - **Validate every input** with Zod schemas; never trust client data. - **Compare tokens and secrets in constant time** (`crypto.timingSafeEqual`). - **Zero keys after use** with `sodium.memzero()`. - **No `eval` or dynamic code execution**: the CSP is strict. - **Never leak internal details in errors.** An error answers problem details with a stable `code`, listed in [PROTOCOL.md](PROTOCOL.md#errors). If you find a vulnerability, report it privately: see [SECURITY.md](SECURITY.md#reporting-a-vulnerability). ## i18n Translations live in `messages/*.json` (10 languages: en, fr, es, de, pt, it, ja, zh, ru, ko). ### Adding a string 1. Add the key to **every** language file: `key-parity.test.ts` fails otherwise. 2. Use `t("key_name")` in Svelte components. 3. Use `t("key_name", { param: value })` for interpolation. ### Adding a language Add its JSON file to `messages/`, then register it in four places: 1. `packages/shared/src/site.ts`: add the code to `LOCALES`, which the server picks a visitor's language from. 2. `apps/web/src/lib/i18n/index.svelte.ts`: import the file and add it to the `messages` record. 3. `apps/web/src/lib/components/LangMenu.svelte`: add it to `LANGS`. 4. `apps/api/src/site/shell.ts`: add its `og:locale` to `OG_LOCALES`. The last three are keyed by `Locale`: once the code is in `LOCALES`, `pnpm typecheck` names whichever of them still lacks it. The sitemap and the pages' `hreflang` links follow `LOCALES` on their own. The API reads every language's file at startup, to write the pages' heads, and refuses to start when `en.json` lacks one of the messages they need (`HEAD_MESSAGES` in `apps/api/src/site/messages.ts`). The other languages fall back to English, message by message. ## Dependencies - pnpm resolves no version published less than two days ago (`minimumReleaseAge` in `pnpm-workspace.yaml`), so a compromised release has time to be yanked before it reaches the lockfile. To take an urgent fix sooner, list its package under `minimumReleaseAgeExclude` for that update. - Dependabot proposes no npm package or action less than three days old. Its npm patch and minor PRs, and its bumps of the base image digest in the Dockerfiles, are labeled `auto-merge-48h` and merged once they're 48 hours old and green. Majors and GitHub Actions updates are reviewed and merged by hand: an action runs with the tokens and secrets of the workflows that use it. ## License By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).