Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/web/src/components/draft-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -839,6 +840,9 @@ function DecryptedShare({
if (openedWithLink) {
return <ReaderLinkCopy />;
}
if (isHostedId(origin)) {
return <HostedShareDialog origin={origin} />;
}
if (canPublish) {
return <ShareDraftDialog origin={origin} />;
}
Expand Down
73 changes: 73 additions & 0 deletions apps/web/src/components/hosted-share-dialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button size="sm" type="button" variant="ghost">
<Share2 data-icon="inline-start" />
Share
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create a reader link</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
<p className="text-muted-foreground text-sm">
Copy these instructions to the agent working with the plan's local
source file. You do not need to collect public keys.
</p>
<DialogFooter showCloseButton>
<Button onClick={handleCopy} type="button">
{copied ? (
<Check data-icon="inline-start" />
) : (
<Share2 data-icon="inline-start" />
)}
{copied ? "Copied" : "Copy agent instructions"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
17 changes: 16 additions & 1 deletion apps/web/src/lib/sharing.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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)}`],
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/lib/sharing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`;
}