From 1d84f324dcdfa10a5bd2f98a6bcff0ef8e03becd Mon Sep 17 00:00:00 2001 From: "pouriade.com" Date: Wed, 21 Oct 2026 17:07:43 -0700 Subject: [PATCH] pnpm setup-env creates .env from .env.example and fills in fresh OAUTH_PRIVATE_KEYS and SECRET_KEY, so local setup is one step instead of copying secrets by hand. It is pnpm keys --write, which never replaces a secret already set, since a new SECRET_KEY can't decrypt stored API keys; plain pnpm keys still prints a pair. --- .env.example | 2 +- CHANGELOG.md | 4 ++++ CLAUDE.md | 4 ++-- README.md | 5 ++--- apps/server/src/keys-cli.ts | 39 +++++++++++++++++++++++++++++++++++-- package.json | 1 + specs/auth.md | 2 +- 7 files changed, 48 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index d991bc0..45ff32f 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ NODE_ENV=development DATABASE_URL=sqlite:./data/scn-chat.sqlite -# Generate both with: pnpm keys +# pnpm setup-env fills in both. pnpm keys prints a fresh pair. SECRET_KEY= OAUTH_PRIVATE_KEYS= # Retired secret keys, comma-separated, kept only to decrypt older values. diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbde84..59c28c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ What changed in each release of SCN Chat, for the people who use it, run it, or ## [Unreleased] +### Added + +- `pnpm setup-env` creates `.env` from `.env.example` and fills in fresh `OAUTH_PRIVATE_KEYS` and `SECRET_KEY`, so local setup no longer means copying secrets by hand. Secrets already set are left alone. + ### Changed - When adding an API key for your own endpoint, the slug now fills in from the name as you type it. You can still edit the slug, and clearing it makes it follow the name again. diff --git a/CLAUDE.md b/CLAUDE.md index 0445312..38cf55e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,8 @@ An AI assistant chat app where each conversation is an atproto permissioned spac - `docs/architecture.md` is the human-facing architecture overview, and `docs/plugins.md` the plugin author guide. `README.md` and everything in `docs/` are for people, so a person has the final word on them. When a change affects what they describe, draft the update and point it out, so the person can rewrite it in their own words. - `CHANGELOG.md` records each change a user, admin, or forker would notice: add its line under Unreleased in the same commit. `docs/releasing.md` has the versioning rules and the steps for cutting a release. - `GLOSSARY.md` holds the project's terms, and `docs/adr/` the decisions behind its less obvious choices. Read the relevant ADR before changing what it decided. -- `pnpm dev` runs the server, which serves the web app through Vite, as one process, and `pnpm dev --scan` also loads React Scan in the browser. `pnpm preview` builds the web app and runs the production build locally. `pnpm dev:remote` and `pnpm preview:remote` do the same behind a localhost.run tunnel over ssh, with the tunnel's HTTPS URL as `PUBLIC_URL`, for testing from a phone. `pnpm start` runs the production build for a deployment. `pnpm check` runs lint, type checking, and every test. `pnpm codegen` regenerates the lexicon code after a lexicon change, and `pnpm publish-lexicons ` publishes the lexicons from the lexicon authority account. `pnpm keys` prints fresh secrets. -- Local setup: copy `.env.example` to `.env`, fill in the secrets from `pnpm keys`, and put your DID in `ADMIN_DIDS`. Plugins, models, roles, and every other runtime setting are configured in the admin area at `/admin`. +- `pnpm dev` runs the server, which serves the web app through Vite, as one process, and `pnpm dev --scan` also loads React Scan in the browser. `pnpm preview` builds the web app and runs the production build locally. `pnpm dev:remote` and `pnpm preview:remote` do the same behind a localhost.run tunnel over ssh, with the tunnel's HTTPS URL as `PUBLIC_URL`, for testing from a phone. `pnpm start` runs the production build for a deployment. `pnpm check` runs lint, type checking, and every test. `pnpm codegen` regenerates the lexicon code after a lexicon change, and `pnpm publish-lexicons ` publishes the lexicons from the lexicon authority account. `pnpm keys` prints fresh secrets, and `pnpm setup-env` writes them into `.env`. +- Local setup: run `pnpm setup-env`, which creates `.env` from `.env.example` with fresh secrets, and put your DID in `ADMIN_DIDS`. Plugins, models, roles, and every other runtime setting are configured in the admin area at `/admin`. - Server tests run on both SQLite and PGlite through `tests/helpers/db.ts`. `tests/helpers/fake-pds.ts` is a fake spaces PDS with real error codes, policies, and signed commits. ## The founding principle diff --git a/README.md b/README.md index 6da7c09..4c08616 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,8 @@ SCN Chat is an ATProto AI assistant chat interface allowing users to use their A ### Setup 1. Install dependencies: `pnpm install` - 2. `cp .env.example .env`. - 3. Run `pnpm keys` to generate secret keys. Copy `OAUTH_PRIVATE_KEYS` and `SECRET_KEY` into your `.env`. - 4. Put your DID in `ADMIN_DIDS` so you can login and configure the application. + 2. Run `pnpm setup-env`. It creates `.env` from `.env.example` and fills in freshly generated `OAUTH_PRIVATE_KEYS` and `SECRET_KEY`, leaving any secret you already set alone. + 3. Put your DID in `ADMIN_DIDS` so you can login and configure the application. ### Start the app diff --git a/apps/server/src/keys-cli.ts b/apps/server/src/keys-cli.ts index c700e34..1ddd967 100644 --- a/apps/server/src/keys-cli.ts +++ b/apps/server/src/keys-cli.ts @@ -1,6 +1,41 @@ import { randomBytes } from 'node:crypto' +import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { parseArgs } from 'node:util' import { JoseKey } from '@atproto/oauth-client-node' +const { values } = parseArgs({ options: { write: { type: 'boolean' } } }) + const key = await JoseKey.generate(['ES256'], `key-${Date.now()}`) -console.log(`OAUTH_PRIVATE_KEYS='${JSON.stringify([key.privateJwk])}'`) -console.log(`SECRET_KEY=${randomBytes(32).toString('base64')}`) +const secrets = { + OAUTH_PRIVATE_KEYS: `'${JSON.stringify([key.privateJwk])}'`, + SECRET_KEY: randomBytes(32).toString('base64'), +} + +if (!values.write) { + for (const [name, value] of Object.entries(secrets)) console.log(`${name}=${value}`) + process.exit(0) +} + +// --write fills the repository's .env, creating it from .env.example first. A secret that is +// already set is never replaced, since a new SECRET_KEY can't decrypt the stored API keys. +const envPath = fileURLToPath(new URL('../../../.env', import.meta.url)) +if (!existsSync(envPath)) { + copyFileSync(fileURLToPath(new URL('../../../.env.example', import.meta.url)), envPath) + console.log('Created .env from .env.example.') +} +let env = readFileSync(envPath, 'utf8') +for (const [name, value] of Object.entries(secrets)) { + const line = new RegExp(`^${name}=(.*)$`, 'm') + const current = env.match(line) + if (current?.[1]) { + console.log(`${name} is already set, so it was left as is.`) + continue + } + env = current + ? env.replace(line, () => `${name}=${value}`) + : `${env.replace(/\n?$/, '\n')}${name}=${value}\n` + console.log(`Wrote ${name}.`) +} +writeFileSync(envPath, env) +if (/^ADMIN_DIDS=$/m.test(env)) console.log('Next, put your DID in ADMIN_DIDS in .env.') diff --git a/package.json b/package.json index 54538e6..d5b197e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "publish-lexicons": "pnpm --filter @scn-chat/lexicons publish-lexicons", "migrate": "pnpm --filter @scn-chat/server migrate", "keys": "pnpm --filter @scn-chat/server keys", + "setup-env": "pnpm --filter @scn-chat/server keys --write", "release": "pnpm --filter @scn-chat/release release", "site-config": "pnpm --filter @scn-chat/server site-config" }, diff --git a/specs/auth.md b/specs/auth.md index 50079b6..2fb3f0f 100644 --- a/specs/auth.md +++ b/specs/auth.md @@ -106,7 +106,7 @@ In the environment: The web session lifetime is the `sessions.ttlDays` admin setting, default 30. A change applies to sessions created or extended after it. -A `pnpm keys` script prints a fresh `OAUTH_PRIVATE_KEYS` value and a `SECRET_KEY` for the providers spec. +A `pnpm keys` script prints a fresh `OAUTH_PRIVATE_KEYS` value and a `SECRET_KEY` for the providers spec. `pnpm setup-env` runs it with `--write`, which creates `.env` from `.env.example` if it is missing and fills in whichever of the two is empty, never replacing one already set. ## Scope Boundaries -- knotmirror 0.0.1