feat(broker): one-off owner_pubkey backfill script
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) <noreply@anthropic.com>
This commit is contained in:
65
apps/broker/scripts/backfill-owner-pubkey.ts
Normal file
65
apps/broker/scripts/backfill-owner-pubkey.ts
Normal file
@@ -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): `<mesh_id> <mesh_slug> <owner_pubkey> <owner_secret_key>`
|
||||
* 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<void> {
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user