From 5a4e45afb373046034647d29725b176c13989814 Mon Sep 17 00:00:00 2001 From: Satchmo <456719+rohenaz@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:17:08 -0400 Subject: [PATCH] Add hosted reader-link sharing handoff --- apps/web/src/components/draft-viewer.tsx | 4 + .../src/components/hosted-share-dialog.tsx | 73 +++++++++++++++++++ apps/web/src/lib/sharing.test.ts | 17 ++++- apps/web/src/lib/sharing.ts | 10 +++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/hosted-share-dialog.tsx diff --git a/apps/web/src/components/draft-viewer.tsx b/apps/web/src/components/draft-viewer.tsx index 4124f1f..0fcfb96 100644 --- a/apps/web/src/components/draft-viewer.tsx +++ b/apps/web/src/components/draft-viewer.tsx @@ -6,6 +6,7 @@ import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; +import { HostedShareDialog } from "@/components/hosted-share-dialog"; import { ShareDraftDialog } from "@/components/share-draft-dialog"; import { ThemeToggle } from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; @@ -839,6 +840,9 @@ function DecryptedShare({ if (openedWithLink) { return ; } + if (isHostedId(origin)) { + return ; + } if (canPublish) { return ; } diff --git a/apps/web/src/components/hosted-share-dialog.tsx b/apps/web/src/components/hosted-share-dialog.tsx new file mode 100644 index 0000000..e2eeb92 --- /dev/null +++ b/apps/web/src/components/hosted-share-dialog.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { Check, Share2 } from "lucide-react"; +import { useCallback, useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { buildReaderLinkInstructions } from "@/lib/sharing"; + +export function HostedShareDialog({ origin }: { origin: string }) { + const [copied, setCopied] = useState(false); + + const handleOpenChange = useCallback((open: boolean) => { + if (!open) { + setCopied(false); + } + }, []); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(buildReaderLinkInstructions(origin)); + setCopied(true); + toast.success("Reader-link instructions copied"); + } catch { + setCopied(false); + toast.error("Could not copy reader-link instructions"); + } + }, [origin]); + + return ( + + + + + + + Create a reader link + + This browser opened the plan with your wallet, so it has no reader + link to copy. The computer that published the plan can add one to a + new version. Anyone with the complete link will be able to read it. + + +

+ Copy these instructions to the agent working with the plan's local + source file. You do not need to collect public keys. +

+ + + +
+
+ ); +} diff --git a/apps/web/src/lib/sharing.test.ts b/apps/web/src/lib/sharing.test.ts index 0fe532e..223cd45 100644 --- a/apps/web/src/lib/sharing.test.ts +++ b/apps/web/src/lib/sharing.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { buildShareInstructions, parseIdentityKeys } from "./sharing"; +import { + buildReaderLinkInstructions, + buildShareInstructions, + parseIdentityKeys, +} from "./sharing"; const READER_A = "02C6047F9441ED7D6D3045406E95C07CD85C778E4B8CEF3CA7ABAC09B95C709EE5"; @@ -34,6 +38,17 @@ describe("sharing instructions", () => { expect(instructions).toContain("BRC-100 wallet"); }); + test("builds hosted reader-link instructions without identity keys", () => { + const instructions = buildReaderLinkInstructions("h_example"); + + expect(instructions).toContain( + "npx bitplan upload ./plan.html --draft h_example --link" + ); + expect(instructions).toContain("locally saved publishing secret"); + expect(instructions).toContain("#k="); + expect(instructions).not.toContain("--share-with"); + }); + test("rejects compressed-looking values that are not canonical curve points", () => { expect(parseIdentityKeys(`02${"f".repeat(64)}`)).toEqual({ invalid: [`02${"f".repeat(64)}`], diff --git a/apps/web/src/lib/sharing.ts b/apps/web/src/lib/sharing.ts index b471bf1..fdebe99 100644 --- a/apps/web/src/lib/sharing.ts +++ b/apps/web/src/lib/sharing.ts @@ -56,3 +56,13 @@ ${flags} Replace ./plan.html with the actual local source path when needed. The CLI preserves the draft's current readers and asks the connected BRC-100 wallet to wrap the shared document key for each reader. Review the wallet prompts and publish the version. By default, the CLI notifies 1Sat for ORDFS capture after the wallet publishes. Do not use --private or --no-relay. Older on-chain versions and their access lists cannot be changed.`; } + +export function buildReaderLinkInstructions(origin: string): string { + return `Create a reader link for the hosted BitPlan at ${origin}. + +On the computer that published this plan, use its local source HTML and run: + +npx bitplan upload ./plan.html --draft ${origin} --link + +Replace ./plan.html with the actual local source path. The BitPlan CLI needs the hosted draft's locally saved publishing secret. It will publish a new version and return a complete URL containing #k=. Anyone with that complete URL can read the plan, so treat it like a password. Do not use --private.`; +}