diff --git a/deploy/gen-oauth-jwk.ts b/deploy/gen-oauth-jwk.ts new file mode 100644 --- /dev/null +++ b/deploy/gen-oauth-jwk.ts @@ -0,0 +1,8 @@ +// Generate an ES256 client-assertion JWK and write it to stdout. +// Used by deploy/setup.sh and ad-hoc re-keying. +// bun run deploy/gen-oauth-jwk.ts > /var/lib/lichen/oauth-private-key.json + +import { generateClientAssertionKey } from "@atcute/oauth-node-client"; + +const jwk = await generateClientAssertionKey("lichen-key-1", "ES256"); +process.stdout.write(`${JSON.stringify(jwk, null, 2)}\n`); diff --git a/deploy/setup.sh b/deploy/setup.sh --- a/deploy/setup.sh +++ b/deploy/setup.sh @@ -40,10 +40,10 @@ fi mkdir -p "$APP_DIR" "$DATA_DIR" "$DATA_DIR/backups" chown "$APP_USER:$APP_USER" "$DATA_DIR" "$DATA_DIR/backups" -KEY_PATH="$DATA_DIR/oauth-private-key.pem" +KEY_PATH="$DATA_DIR/oauth-private-key.json" if [ ! -f "$KEY_PATH" ]; then - echo "==> Generating ES256 private key" - openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out "$KEY_PATH" + echo "==> Generating ES256 client-assertion JWK" + sudo -u "$APP_USER" bash -c "cd $APP_DIR && /usr/local/bin/bun run deploy/gen-oauth-jwk.ts" > "$KEY_PATH" chown "$APP_USER:$APP_USER" "$KEY_PATH" chmod 600 "$KEY_PATH" fi @@ -71,10 +71,10 @@ STAGING_DATA_DIR="/var/lib/lichen-staging" mkdir -p "$STAGING_APP_DIR" "$STAGING_DATA_DIR" chown "$APP_USER:$APP_USER" "$STAGING_DATA_DIR" -STAGING_KEY_PATH="$STAGING_DATA_DIR/oauth-private-key.pem" +STAGING_KEY_PATH="$STAGING_DATA_DIR/oauth-private-key.json" if [ ! -f "$STAGING_KEY_PATH" ]; then - echo "==> Generating staging ES256 private key" - openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out "$STAGING_KEY_PATH" + echo "==> Generating staging ES256 client-assertion JWK" + sudo -u "$APP_USER" bash -c "cd $APP_DIR && /usr/local/bin/bun run deploy/gen-oauth-jwk.ts" > "$STAGING_KEY_PATH" chown "$APP_USER:$APP_USER" "$STAGING_KEY_PATH" chmod 600 "$STAGING_KEY_PATH" fi @@ -85,7 +85,7 @@ echo "==> Creating staging environment file" mkdir -p /etc/lichen-staging cat > "$STAGING_ENV_FILE" <<'EOF' PUBLIC_URL=https://staging.lichen.wiki -OAUTH_PRIVATE_KEY_PATH=/var/lib/lichen-staging/oauth-private-key.pem +OAUTH_PRIVATE_KEY_PATH=/var/lib/lichen-staging/oauth-private-key.json RELAY_URL=wss://bsky.network DB_PATH=/var/lib/lichen-staging/lichen.db PORT=3001 diff --git a/knip.config.ts b/knip.config.ts --- a/knip.config.ts +++ b/knip.config.ts @@ -5,6 +5,7 @@ entry: [ "src/**/index.ts", "public/editor/editor.ts", "public/viz/viz-hydrate.ts", + "deploy/gen-oauth-jwk.ts", ], project: ["**/*.{ts,tsx,mjs}!"], ignoreDependencies: ["tailwindcss", "@tailwindcss/typography", "htmx.org"], diff --git a/src/atproto/client.ts b/src/atproto/client.ts --- a/src/atproto/client.ts +++ b/src/atproto/client.ts @@ -10,7 +10,7 @@ } from "@atcute/identity-resolver"; import { NodeDnsHandleResolver } from "@atcute/identity-resolver-node"; import type { Store } from "@atcute/oauth-node-client"; import { - importClientAssertionPkcs8, + type ClientAssertionPrivateJwk, OAuthClient, type StoredSession, type StoredState, @@ -107,14 +107,9 @@ }); } export async function createOAuthClient(env: AtprotoEnv): Promise { - // Convert PEM (PKCS#8 ES256) → JWK in-memory at load. - // Future: store the key as a JWK file directly and skip this step - // (requires generating + persisting a fresh JWK and wiping oauth_sessions). - const pem = readFileSync(env.privateKeyPath, "utf-8"); - const privateJwk = await importClientAssertionPkcs8(pem, { - kid: "lichen-key-1", - alg: "ES256", - }); + const privateJwk = JSON.parse( + readFileSync(env.privateKeyPath, "utf-8"), + ) as ClientAssertionPrivateJwk; const url = env.publicUrl; return new OAuthClient({ diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -223,7 +223,18 @@ updated_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (did, slug) ) `); - db.run("INSERT INTO wikis_new SELECT * FROM wikis"); + // Explicit column list: old wikis added theme_mode/theme via ALTER TABLE, + // so they're appended at the end while wikis_new has them in the middle. + db.run(` + INSERT INTO wikis_new ( + slug, did, name, visibility, language, description, + theme_mode, theme, at_uri, created_at, updated_at + ) + SELECT + slug, did, name, visibility, language, description, + theme_mode, theme, at_uri, created_at, updated_at + FROM wikis + `); db.run("DROP TABLE wikis"); db.run("ALTER TABLE wikis_new RENAME TO wikis"); db.run("CREATE INDEX IF NOT EXISTS idx_wikis_did ON wikis(did)");