From fa23525c46588542ece83ae2257476f4d9a71f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:08:07 +0100 Subject: [PATCH] feat(broker): one-off owner_pubkey backfill script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Populates mesh.mesh.owner_pubkey for pre-18c rows by generating a fresh ed25519 keypair per mesh + emitting the secret key to stdout for out-of-band hand-off. Idempotent: only patches rows WHERE owner_pubkey IS NULL. Machine- readable output (tab-separated: mesh_id, slug, pubkey, secret_key) so operators can pipe into a secure store. Usage: DATABASE_URL=... bun apps/broker/scripts/backfill-owner-pubkey.ts > owners.tsv # then securely distribute secrets to mesh owners Verified locally: nulled smoke-test mesh's owner_pubkey → ran backfill → fresh keypair written, secret emitted. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/broker/scripts/backfill-owner-pubkey.ts | 65 ++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/broker/scripts/backfill-owner-pubkey.ts diff --git a/apps/broker/scripts/backfill-owner-pubkey.ts b/apps/broker/scripts/backfill-owner-pubkey.ts new file mode 100644 index 0000000..355b28a --- /dev/null +++ b/apps/broker/scripts/backfill-owner-pubkey.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env bun +/** + * One-off backfill: populate `mesh.mesh.owner_pubkey` for meshes + * created before Step 18c landed. + * + * Runs idempotently: only touches rows where owner_pubkey IS NULL. + * Generates a fresh ed25519 keypair per mesh and writes the owner + * SECRET KEY to stdout (paired with mesh_id) so an operator can + * hand it back to the mesh owner out-of-band. + * + * Usage: + * DATABASE_URL=... bun apps/broker/scripts/backfill-owner-pubkey.ts + * + * Output format (per row): ` ` + * Redirect stdout to a secure file — the secret keys grant admin + * invite-signing power and must be stored carefully. + */ + +import sodium from "libsodium-wrappers"; +import { eq, isNull } from "drizzle-orm"; +import { db } from "../src/db"; +import { mesh } from "@turbostarter/db/schema/mesh"; + +async function main(): Promise { + await sodium.ready; + + const missing = await db + .select({ id: mesh.id, slug: mesh.slug, name: mesh.name }) + .from(mesh) + .where(isNull(mesh.ownerPubkey)); + + if (missing.length === 0) { + console.error("[backfill] no rows to patch"); + return; + } + console.error(`[backfill] patching ${missing.length} mesh(es)`); + + for (const row of missing) { + const kp = sodium.crypto_sign_keypair(); + const pubHex = sodium.to_hex(kp.publicKey); + const secHex = sodium.to_hex(kp.privateKey); + await db + .update(mesh) + .set({ ownerPubkey: pubHex }) + .where(eq(mesh.id, row.id)); + // stdout: machine-readable, one mesh per line + console.log(`${row.id}\t${row.slug}\t${pubHex}\t${secHex}`); + console.error( + `[backfill] patched mesh "${row.slug}" (${row.id}) — save its secret key`, + ); + } + console.error( + "[backfill] done. SECURELY HAND OFF secret keys to mesh owners.", + ); +} + +main() + .then(() => process.exit(0)) + .catch((e) => { + console.error( + "[backfill] error:", + e instanceof Error ? e.message : String(e), + ); + process.exit(1); + });