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 { ImpersonatingBanner } from "~/modules/admin/users/user/impersonating-banner";
|
||||||
import { BaseLayout } from "~/modules/common/layout/base";
|
import { BaseLayout } from "~/modules/common/layout/base";
|
||||||
import { Toaster } from "~/modules/common/toast";
|
import { Toaster } from "~/modules/common/toast";
|
||||||
import { BuyCtaDialog } from "~/modules/marketing/layout/buy-cta-dialog";
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return config.locales.map((locale) => ({ locale }));
|
return config.locales.map((locale) => ({ locale }));
|
||||||
@@ -33,7 +32,6 @@ export default async function RootLayout({
|
|||||||
<Providers locale={locale}>
|
<Providers locale={locale}>
|
||||||
<ImpersonatingBanner />
|
<ImpersonatingBanner />
|
||||||
{children}
|
{children}
|
||||||
<BuyCtaDialog />
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</Providers>
|
</Providers>
|
||||||
</BaseLayout>
|
</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,
|
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,
|
url,
|
||||||
canonical,
|
canonical,
|
||||||
images = [DEFAULT_IMAGE],
|
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