Compare commits
2 Commits
e6e76d1b9a
...
533dcc11f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
533dcc11f6 | ||
|
|
fa23525c46 |
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);
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import { Providers } from "~/lib/providers/providers";
|
||||
import { ImpersonatingBanner } from "~/modules/admin/users/user/impersonating-banner";
|
||||
import { BaseLayout } from "~/modules/common/layout/base";
|
||||
import { Toaster } from "~/modules/common/toast";
|
||||
import { BuyCtaDialog } from "~/modules/marketing/layout/buy-cta-dialog";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return config.locales.map((locale) => ({ locale }));
|
||||
@@ -33,7 +32,6 @@ export default async function RootLayout({
|
||||
<Providers locale={locale}>
|
||||
<ImpersonatingBanner />
|
||||
{children}
|
||||
<BuyCtaDialog />
|
||||
<Toaster />
|
||||
</Providers>
|
||||
</BaseLayout>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 947 KiB |
@@ -49,7 +49,7 @@ export const getMetadata =
|
||||
(
|
||||
{
|
||||
title,
|
||||
description = "common:product.description",
|
||||
description = "Connect your Claude Code sessions to each other. Zero config. End-to-end encrypted. Peer mesh for Claude Code teams.",
|
||||
url,
|
||||
canonical,
|
||||
images = [DEFAULT_IMAGE],
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { buttonVariants } from "@turbostarter/ui-web/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@turbostarter/ui-web/dialog";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
const MIN_DELAY_MS = 15_000;
|
||||
const STORAGE_LAST_SHOWN_AT = "buyCtaDialog:lastShownAt";
|
||||
const STORAGE_PREV_DELAY_MS = "buyCtaDialog:prevDelayMs";
|
||||
|
||||
export const BuyCtaDialog = () => {
|
||||
const { t } = useTranslation(["common", "marketing"]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const scheduleNext = () => {
|
||||
const now = Date.now();
|
||||
const storedLastShown = Number(
|
||||
window.localStorage.getItem(STORAGE_LAST_SHOWN_AT) ?? "0",
|
||||
);
|
||||
const prevDelayMs = Number(
|
||||
window.localStorage.getItem(STORAGE_PREV_DELAY_MS) ?? "0",
|
||||
);
|
||||
|
||||
const nextDelay = Math.max(
|
||||
MIN_DELAY_MS,
|
||||
prevDelayMs ? prevDelayMs * 2 : MIN_DELAY_MS,
|
||||
);
|
||||
|
||||
const baseNextShow = storedLastShown
|
||||
? storedLastShown + nextDelay
|
||||
: now + nextDelay;
|
||||
|
||||
const delayFromNow = Math.max(MIN_DELAY_MS, baseNextShow - now);
|
||||
|
||||
if (timeoutIdRef.current) {
|
||||
window.clearTimeout(timeoutIdRef.current);
|
||||
}
|
||||
|
||||
timeoutIdRef.current = window.setTimeout(() => {
|
||||
setOpen(true);
|
||||
|
||||
const shownAt = Date.now();
|
||||
window.localStorage.setItem(STORAGE_LAST_SHOWN_AT, String(shownAt));
|
||||
window.localStorage.setItem(STORAGE_PREV_DELAY_MS, String(nextDelay));
|
||||
|
||||
scheduleNext();
|
||||
}, delayFromNow);
|
||||
};
|
||||
|
||||
scheduleNext();
|
||||
|
||||
return () => {
|
||||
if (timeoutIdRef.current) {
|
||||
window.clearTimeout(timeoutIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader className="space-y-3">
|
||||
<DialogTitle>{t("cta.buy.question")}</DialogTitle>
|
||||
<DialogDescription className="text-foreground text-base">
|
||||
{t("cta.buy.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<a
|
||||
href="https://turbostarter.dev/#pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants(), "gap-2")}
|
||||
>
|
||||
<Icons.Code className="size-4" />
|
||||
{t("cta.buy.button")}
|
||||
</a>
|
||||
|
||||
<div className="bg-border relative -mx-6 my-3 h-px">
|
||||
<span className="bg-background text-muted-foreground absolute left-1/2 -translate-x-1/2 -translate-y-1/2 px-3 text-sm">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>{t("cta.buy.join.description")}</p>
|
||||
|
||||
<a
|
||||
className={cn(
|
||||
buttonVariants(),
|
||||
"gap-2 bg-[#5865F2] px-7 no-underline hover:bg-[#5865F2]/95",
|
||||
)}
|
||||
href="https://discord.gg/KjpK2uk3JP"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Icons.Discord className="size-[1.35rem] text-white" />
|
||||
<span className="font-semibold text-white">
|
||||
{t("cta.buy.join.button")}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user