diff --git a/packages/demo/next.config.mjs b/packages/demo/next.config.mjs
index c3f0428b2..cc4b61b29 100644
--- a/packages/demo/next.config.mjs
+++ b/packages/demo/next.config.mjs
@@ -3,6 +3,24 @@ const nextConfig = {
experimental: {
optimizePackageImports: ["@ckb-ccc/core", "@ckb-ccc/core/bundle"],
},
+ // SharedArrayBuffer (required by @nervosnetwork/fiber-js WASM) needs
+ // Cross-Origin-Opener-Policy: same-origin + COEP on the page.
+ //
+ // COOP: same-origin severs window.opener in cross-origin wallet popups
+ // (JoyID, etc.), so it is applied only to /connected/Fiber where the WASM
+ // runs. Wallet signing is delegated to /fiber-sign-proxy (a same-origin
+ // popup with no COOP) that relays the signature back via BroadcastChannel.
+ async headers() {
+ return [
+ {
+ source: "/(.*)",
+ headers: [
+ { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
+ { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
+ ],
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/packages/demo/package.json b/packages/demo/package.json
index b96bf67a2..b03d5e268 100644
--- a/packages/demo/package.json
+++ b/packages/demo/package.json
@@ -36,6 +36,7 @@
"@eslint/eslintrc": "^3.3.1",
"@headlessui/react": "^2.2.7",
"@heroicons/react": "^2.2.0",
+ "@nervosnetwork/fiber-js": "0.8.1",
"@scure/bip32": "^2.0.0",
"@scure/bip39": "^2.0.0",
"@tailwindcss/postcss": "^4.1.12",
diff --git a/packages/demo/src/app/connected/(tools)/Fiber/components.tsx b/packages/demo/src/app/connected/(tools)/Fiber/components.tsx
new file mode 100644
index 000000000..a4c77dc5d
--- /dev/null
+++ b/packages/demo/src/app/connected/(tools)/Fiber/components.tsx
@@ -0,0 +1,583 @@
+import { Button } from "@/src/components/Button";
+import { TextInput } from "@/src/components/Input";
+import { ccc } from "@ckb-ccc/connector-react";
+import { Copy, GitBranch, Send, Terminal, Users } from "lucide-react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { hexToCkb } from "./config";
+import type {
+ FjChannel,
+ FjGetInvoice,
+ FjInvoice,
+ FjPeer,
+ LogEntry,
+ LogLevel,
+ Tab,
+} from "./types";
+
+// ── Layout primitives ─────────────────────────────────────────────────────────
+
+export function Card({
+ children,
+ className,
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function InfoCard({
+ label,
+ value,
+ mono,
+}: {
+ label: string;
+ value: string;
+ mono?: boolean;
+}) {
+ return (
+
+
{label}
+
+ {value}
+
+
+ );
+}
+
+// ── Log panel ─────────────────────────────────────────────────────────────────
+
+const LOG_TEXT: Record = {
+ info: "text-sky-400",
+ warn: "text-amber-400",
+ error: "text-rose-400",
+ success: "text-emerald-400",
+};
+
+const LOG_LABEL: Record = {
+ info: "INFO",
+ warn: "WARN",
+ error: "ERR!",
+ success: "OK ",
+};
+
+const LOG_LABEL_COLOR: Record = {
+ info: "text-sky-500",
+ warn: "text-amber-500",
+ error: "text-rose-500",
+ success: "text-emerald-500",
+};
+
+const FILTERS: { key: LogLevel | "all"; label: string }[] = [
+ { key: "all", label: "All" },
+ { key: "info", label: "Info" },
+ { key: "warn", label: "Warn" },
+ { key: "error", label: "Error" },
+ { key: "success", label: "OK" },
+];
+
+export function LogPanel({
+ logs,
+ onClear,
+}: {
+ logs: LogEntry[];
+ onClear: () => void;
+}) {
+ const [filter, setFilter] = useState("all");
+ const scrollRef = useRef(null);
+ const autoScrollRef = useRef(true);
+
+ const handleScroll = useCallback(() => {
+ const el = scrollRef.current;
+ if (!el) {
+ return;
+ }
+ autoScrollRef.current =
+ el.scrollHeight - el.scrollTop - el.clientHeight < 32;
+ }, []);
+
+ useEffect(() => {
+ if (autoScrollRef.current && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [logs]);
+
+ const filtered =
+ filter === "all" ? logs : logs.filter((e) => e.level === filter);
+
+ const counts = (["info", "warn", "error", "success"] as LogLevel[]).reduce(
+ (acc, lvl) => {
+ acc[lvl] = logs.filter((e) => e.level === lvl).length;
+ return acc;
+ },
+ {} as Record,
+ );
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Node Logs
+
+ {logs.length} lines
+
+ Fiber WASM logs → DevTools (F12) Console
+
+
+
+ {FILTERS.map(({ key, label }) => {
+ const count = key === "all" ? logs.length : counts[key];
+ const active = filter === key;
+ return (
+
setFilter(key)}
+ className={`rounded px-2 py-0.5 text-xs font-medium transition-colors ${
+ active
+ ? "bg-gray-600 text-gray-100"
+ : "text-gray-500 hover:text-gray-300"
+ }`}
+ >
+ {label}
+ {count > 0 && (
+ {count}
+ )}
+
+ );
+ })}
+
+
+ Clear
+
+
+
+
+ {/* Log body */}
+
+ {filtered.length === 0 ? (
+
+ {filter === "all" ? "No logs yet." : `No ${filter} entries.`}
+
+ ) : (
+ filtered.map((e) => (
+
+ {e.time}
+
+ {LOG_LABEL[e.level]}
+
+ {e.msg}
+
+ ))
+ )}
+
+
+ );
+}
+
+// ── Tab bar ───────────────────────────────────────────────────────────────────
+
+const TABS: { key: Tab; label: string }[] = [
+ { key: "peers", label: "Peers" },
+ { key: "channels", label: "Channels" },
+ { key: "invoices", label: "Invoices" },
+ { key: "payments", label: "Payments" },
+];
+
+export function TabBar({
+ active,
+ onChange,
+}: {
+ active: Tab;
+ onChange: (t: Tab) => void;
+}) {
+ return (
+
+ {TABS.map(({ key, label }) => (
+ onChange(key)}
+ className={`flex-1 py-3 text-sm font-medium transition-colors ${
+ active === key
+ ? "border-b-2 border-neutral-700 text-neutral-700"
+ : "text-gray-400 hover:text-gray-600"
+ }`}
+ >
+ {label}
+
+ ))}
+
+ );
+}
+
+// ── Peers tab ─────────────────────────────────────────────────────────────────
+
+export function PeersTab({
+ peers,
+ onConnect,
+ onDisconnect,
+}: {
+ peers: FjPeer[];
+ onConnect: (addr: string) => Promise;
+ onDisconnect: (peerId: string) => Promise;
+}) {
+ const [addr, setAddr] = useState("");
+ return (
+
+
+
+
+
+
+ onConnect(addr).then(() => setAddr(""))}
+ >
+ Connect
+
+
+
+
+ Connected peers ({peers.length})
+
+ {peers.length === 0 ? (
+
No peers connected.
+ ) : (
+ peers.map((p) => (
+
+
+
+ {p.pubkey}
+
+
+ {p.address}
+
+
+
onDisconnect(p.pubkey)}
+ >
+ Disconnect
+
+
+ ))
+ )}
+
+ );
+}
+
+// ── Channels tab ──────────────────────────────────────────────────────────────
+
+export function ChannelsTab({
+ channels,
+ onOpen,
+ onClose,
+}: {
+ channels: FjChannel[];
+ onOpen: (peerId: string, amount: string, isPublic: boolean) => Promise;
+ onClose: (channelId: string) => Promise;
+}) {
+ const [peerId, setPeerId] = useState("");
+ const [amount, setAmount] = useState("");
+ const [isPublic, setIsPublic] = useState(true);
+
+ return (
+
+ {/* Open channel form */}
+
+
Open Channel
+
+
+
+
+ setIsPublic(e.currentTarget.checked)}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+ Public channel
+
+
+ onOpen(peerId, amount, isPublic).then(() => {
+ setPeerId("");
+ setAmount("");
+ })
+ }
+ >
+ Open
+
+
+
+
+ {/* Channel list */}
+
+ Channels ({channels.length})
+
+ {channels.length === 0 ? (
+
No channels.
+ ) : (
+ channels.map((ch) => (
+
+
+
+ {ch.channel_id}
+
+
+
+ {ch.state.state_name}
+
+ onClose(ch.channel_id)}
+ >
+ Close
+
+
+
+
+ Local: {hexToCkb(ch.local_balance)} CKB
+ Remote: {hexToCkb(ch.remote_balance)} CKB
+
+ Peer: {ch.pubkey}
+
+
+
+ ))
+ )}
+
+ );
+}
+
+// ── Invoices tab ──────────────────────────────────────────────────────────────
+
+const INVOICE_STATUS_COLOR: Record = {
+ Paid: "bg-green-100 text-green-700",
+ Open: "bg-blue-100 text-blue-700",
+};
+
+export function InvoicesTab({
+ onNew,
+ onCheck,
+}: {
+ onNew: (amount: string, desc: string) => Promise;
+ onCheck: (paymentHash: string) => Promise;
+}) {
+ const [amount, setAmount] = useState("");
+ const [desc, setDesc] = useState("");
+ const [invoiceAddr, setInvoiceAddr] = useState("");
+ const [hash, setHash] = useState("");
+ const [status, setStatus] = useState("");
+
+ return (
+
+
+
Create Invoice
+
+
+
+
+ onNew(amount, desc).then((r) => setInvoiceAddr(r.invoice_address))
+ }
+ >
+ Create
+
+
+ {invoiceAddr && (
+
+
+ {invoiceAddr}
+
+
navigator.clipboard.writeText(invoiceAddr)}
+ >
+
+
+
+ )}
+
+
+
+
+ Check Invoice Status
+
+
+
+ onCheck(hash).then((r) => setStatus(r.status))}
+ >
+ Check
+
+ {status && (
+
+ {status}
+
+ )}
+
+
+
+ );
+}
+
+// ── Payments tab ──────────────────────────────────────────────────────────────
+
+export function PaymentsTab({
+ onSend,
+}: {
+ onSend: (invoice: string) => Promise;
+}) {
+ const [invoice, setInvoice] = useState("");
+ const [result, setResult] = useState("");
+
+ return (
+
+
+
+
+ onSend(invoice)
+ .then(() => setResult("Payment sent"))
+ .catch((e: unknown) =>
+ setResult(
+ `Failed: ${e instanceof Error ? e.message : String(e)}`,
+ ),
+ )
+ }
+ >
+ Send Payment
+
+
+ {result && (
+
+ )}
+
+ );
+}
+
+// ── Node info grid ────────────────────────────────────────────────────────────
+
+function hexToNum(hex: string): string {
+ return String(Number(ccc.numFrom(hex)));
+}
+
+export function NodeInfoGrid({
+ nodeId,
+ addresses,
+ peersCount,
+ channelCount,
+ pendingChannelCount,
+}: {
+ nodeId: string;
+ addresses: string[];
+ peersCount: string;
+ channelCount: string;
+ pendingChannelCount: string;
+}) {
+ return (
+
+
+
+
+
+
+
+ {addresses.length > 0 && (
+
+
Listening Addresses
+ {addresses.map((a, i) => (
+
+ {a}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/packages/demo/src/app/connected/(tools)/Fiber/config.ts b/packages/demo/src/app/connected/(tools)/Fiber/config.ts
new file mode 100644
index 000000000..46f0bba29
--- /dev/null
+++ b/packages/demo/src/app/connected/(tools)/Fiber/config.ts
@@ -0,0 +1,30 @@
+import { ccc } from "@ckb-ccc/connector-react";
+
+// ── localStorage keys ─────────────────────────────────────────────────────────
+
+export const LS_MANUAL_CONFIG = "fiber-demo-manual-config";
+
+export const SIGN_MESSAGE = "Fiber Demo Node Key v1";
+
+export function lsNodeKeyFor(walletAddr: string): string {
+ return `fiber-demo-nodekey-${walletAddr}`;
+}
+
+export function readLs(key: string, fallback = ""): string {
+ if (typeof window === "undefined") return fallback;
+ return localStorage.getItem(key) ?? fallback;
+}
+
+export function writeLs(key: string, value: string): void {
+ if (typeof window !== "undefined") localStorage.setItem(key, value);
+}
+
+// ── Display helpers ───────────────────────────────────────────────────────────
+
+export function hexToCkb(hex: string): string {
+ return ccc.fixedPointToString(ccc.numFrom(hex), 8);
+}
+
+export function maskKey(key: string): string {
+ return `${key.slice(0, 10)}···${key.slice(-6)}`;
+}
diff --git a/packages/demo/src/app/connected/(tools)/Fiber/hooks.ts b/packages/demo/src/app/connected/(tools)/Fiber/hooks.ts
new file mode 100644
index 000000000..753ab2de4
--- /dev/null
+++ b/packages/demo/src/app/connected/(tools)/Fiber/hooks.ts
@@ -0,0 +1,532 @@
+import { ccc } from "@ckb-ccc/connector-react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { lsNodeKeyFor, readLs, writeLs } from "./config";
+import type {
+ CkbRpcScript,
+ CkbRpcTransaction,
+ FiberInstance,
+ FjChannel,
+ FjGetInvoice,
+ FjInvoice,
+ FjNodeInfo,
+ FjOpenChannel,
+ FjPayment,
+ FjPeer,
+ LogEntry,
+ LogLevel,
+} from "./types";
+
+// ── Shared helpers ────────────────────────────────────────────────────────────
+
+function errMsg(e: unknown): string {
+ return e instanceof Error ? e.message : String(e);
+}
+
+async function resolveLockCellDeps(
+ client: ccc.Client,
+ lock: ccc.Script,
+): Promise {
+ const infos = await Promise.all(
+ Object.values(ccc.KnownScript).map((ks) => client.getKnownScript(ks)),
+ );
+ for (const info of infos) {
+ if (info.codeHash !== lock.codeHash || info.hashType !== lock.hashType) {
+ continue;
+ }
+ const deps = await client.getCellDeps(...info.cellDeps);
+ return deps.map((dep) => ({
+ dep_type: dep.depType === "depGroup" ? "dep_group" : ("code" as const),
+ out_point: {
+ tx_hash: ccc.hexFrom(dep.outPoint.txHash),
+ index: ccc.numToHex(dep.outPoint.index),
+ },
+ }));
+ }
+ return [];
+}
+
+const MAX_LOGS = 500;
+
+// ── Activity log ──────────────────────────────────────────────────────────────
+
+type AddLog = (level: LogLevel, msg: string) => void;
+
+export function useActivityLog() {
+ const [logs, setLogs] = useState([]);
+ const idRef = useRef(0);
+
+ const addLog = useCallback((level: LogLevel, msg: string) => {
+ const id = ++idRef.current;
+ const time = new Date().toLocaleTimeString("en", { hour12: false });
+ setLogs((prev) => [...prev, { id, level, time, msg }].slice(-MAX_LOGS));
+ }, []);
+
+ return { logs, addLog, clearLogs: () => setLogs([]) };
+}
+
+// ── Node identity key ─────────────────────────────────────────────────────────
+
+export function useNodeKey(signer: ccc.Signer | undefined, addLog: AddLog) {
+ const [walletAddr, setWalletAddr] = useState("");
+ const [storedKey, setStoredKey] = useState(null);
+
+ useEffect(() => {
+ if (!signer) {
+ return;
+ }
+ let cancelled = false;
+ signer.getInternalAddress().then((addr) => {
+ if (cancelled) {
+ return;
+ }
+ setWalletAddr(addr);
+ const saved = readLs(lsNodeKeyFor(addr));
+ if (saved) {
+ setStoredKey(saved);
+ addLog("info", `Loaded stored node key for ${addr.slice(0, 12)}…`);
+ }
+ });
+ return () => {
+ cancelled = true;
+ setWalletAddr("");
+ setStoredKey(null);
+ };
+ }, [signer, addLog]);
+
+ const deriveKeys = useCallback(
+ async (message: string): Promise => {
+ if (!signer || !walletAddr) return null;
+ addLog("info", `Signing "${message}"…`);
+ try {
+ const sig = await signer.signMessage(message);
+ const sigBytes = ccc.bytesFrom(sig.signature);
+ const fiberKeyHex = ccc.hashCkb(sigBytes);
+ setStoredKey(fiberKeyHex);
+ writeLs(lsNodeKeyFor(walletAddr), fiberKeyHex);
+ addLog("success", "Node identity key derived and persisted.");
+ return ccc.bytesFrom(fiberKeyHex);
+ } catch (e) {
+ addLog("error", `Key derivation failed: ${errMsg(e)}`);
+ return null;
+ }
+ },
+ [signer, walletAddr, addLog],
+ );
+
+ // Avoids wallet pop-ups on COOP-restricted pages; uses the already-stored key.
+ const keysFromStored = useCallback((): Uint8Array | null => {
+ if (!storedKey) return null;
+ return ccc.bytesFrom(storedKey);
+ }, [storedKey]);
+
+ return { walletAddr, storedKey, deriveKeys, keysFromStored };
+}
+
+// ── Fiber node lifecycle & RPC ────────────────────────────────────────────────
+
+export interface StartOptions {
+ fiberKey: Uint8Array;
+ dbPrefix: string;
+ configYaml: string | undefined;
+}
+
+export function useFiberNode(signer: ccc.Signer | undefined, addLog: AddLog) {
+ const fiberRef = useRef(null);
+ const [isRunning, setIsRunning] = useState(false);
+ const [isStarting, setIsStarting] = useState(false);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [nodeInfo, setNodeInfo] = useState(null);
+ const [peers, setPeers] = useState([]);
+ const [channels, setChannels] = useState([]);
+
+ useEffect(
+ () => () => {
+ fiberRef.current?.stop().catch(() => undefined);
+ },
+ [],
+ );
+
+ // Redirect console output to the activity log while the node is running.
+ useEffect(() => {
+ if (!isRunning) {
+ return;
+ }
+ const levels = [
+ ["log", "info"],
+ ["warn", "warn"],
+ ["error", "error"],
+ ] as const;
+ const originals = levels.map(([method]) => console[method].bind(console));
+ levels.forEach(([method, level], i) => {
+ console[method] = (...a: unknown[]) => {
+ originals[i](...a);
+ addLog(level, a.map(String).join(" "));
+ };
+ });
+ return () => {
+ levels.forEach(([method], i) => {
+ console[method] = originals[i];
+ });
+ };
+ }, [isRunning, addLog]);
+
+ function invoke(name: string, args?: unknown[]): Promise {
+ if (!fiberRef.current) throw new Error("Fiber node is not running");
+ return fiberRef.current.invokeCommand(name, args) as Promise;
+ }
+
+ const logResponse = useCallback(
+ (method: string, result: unknown) => {
+ const text =
+ result === undefined || result === null
+ ? "null"
+ : JSON.stringify(result);
+ addLog("success", `${method} → ${text}`);
+ },
+ [addLog],
+ );
+
+ function resetState() {
+ setIsRunning(false);
+ setNodeInfo(null);
+ setPeers([]);
+ setChannels([]);
+ }
+
+ const refreshNodeData = useCallback(async () => {
+ if (!fiberRef.current) {
+ return;
+ }
+ setIsRefreshing(true);
+ addLog("info", "Refreshing node data…");
+ try {
+ const [info, peerRes, chanRes] = await Promise.all([
+ invoke("node_info"),
+ invoke<{ peers: FjPeer[] }>("list_peers"),
+ invoke<{ channels: FjChannel[] }>("list_channels", [{}]),
+ ]);
+ logResponse("node_info", info);
+ logResponse("list_peers", peerRes);
+ logResponse("list_channels", chanRes);
+ setNodeInfo(info);
+ setPeers(peerRes.peers ?? []);
+ setChannels(chanRes.channels ?? []);
+ } catch (e) {
+ addLog("error", `Refresh failed: ${errMsg(e)}`);
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, [addLog, logResponse]);
+
+ const startNode = useCallback(
+ async (opts: StartOptions) => {
+ setIsStarting(true);
+ try {
+ if (!opts.configYaml) {
+ addLog(
+ "warn",
+ "No YAML config provided — please fill in the Node Configuration before starting.",
+ );
+ return;
+ }
+ addLog("info", "Starting fiber node…");
+ const { Fiber } = await import("@nervosnetwork/fiber-js");
+ const fiber = new Fiber();
+ await fiber.start(
+ opts.configYaml,
+ opts.fiberKey,
+ undefined,
+ undefined,
+ "info",
+ opts.dbPrefix,
+ );
+ const readyProbe = await fiber.invokeCommand("list_channels", [{}]);
+ fiberRef.current = fiber;
+ logResponse("list_channels (ready probe)", readyProbe);
+ setIsRunning(true);
+ await refreshNodeData();
+ } catch (e) {
+ addLog("error", `Node start failed: ${errMsg(e)}`);
+ throw e;
+ } finally {
+ setIsStarting(false);
+ }
+ },
+ [addLog, logResponse, refreshNodeData],
+ );
+
+ const stopNode = useCallback(async () => {
+ addLog("info", "Stopping fiber node…");
+ await fiberRef.current?.stop().catch(() => undefined);
+ fiberRef.current = null;
+ resetState();
+ addLog("info", "Fiber node stopped.");
+ }, [addLog]);
+
+ const clearNodeData = useCallback(
+ async (dbPrefix: string) => {
+ if (fiberRef.current) {
+ addLog("info", "Stopping fiber node before clearing data…");
+ await fiberRef.current.stop().catch(() => undefined);
+ fiberRef.current = null;
+ resetState();
+ }
+ addLog("info", `Deleting IndexedDB databases with prefix "${dbPrefix}"…`);
+ try {
+ const dbs = await indexedDB.databases();
+ const targets = dbs.filter((d) => d.name?.startsWith(dbPrefix));
+ if (targets.length === 0) {
+ addLog("warn", "No fiber databases found for this wallet.");
+ return;
+ }
+ await Promise.all(
+ targets.map(
+ (d) =>
+ new Promise((resolve, reject) => {
+ const req = indexedDB.deleteDatabase(d.name!);
+ req.onsuccess = () => resolve();
+ req.onerror = () => reject(req.error);
+ req.onblocked = () =>
+ addLog(
+ "warn",
+ `Deletion of "${d.name}" is blocked — close other tabs.`,
+ );
+ }),
+ ),
+ );
+ addLog(
+ "success",
+ `Cleared ${targets.length} database(s). Start the node to begin fresh.`,
+ );
+ } catch (e) {
+ addLog("error", `Failed to clear data: ${errMsg(e)}`);
+ }
+ },
+ [addLog],
+ );
+
+ const connectPeer = useCallback(
+ async (address: string) => {
+ addLog("info", `Connecting to peer: ${address}`);
+ const connectResult = await invoke("connect_peer", [
+ { address, save: true },
+ ]);
+ logResponse("connect_peer", connectResult);
+ },
+ [addLog, logResponse],
+ );
+
+ const disconnectPeer = useCallback(
+ async (peerId: string) => {
+ addLog("info", `Disconnecting peer: ${peerId.slice(0, 20)}…`);
+ const disconnectResult = await invoke("disconnect_peer", [
+ { pubkey: peerId },
+ ]);
+ logResponse("disconnect_peer", disconnectResult);
+ },
+ [addLog, logResponse],
+ );
+
+ const openChannel = useCallback(
+ async (
+ peerId: string,
+ fundingAmount: string,
+ isPublic: boolean,
+ ): Promise => {
+ if (!signer) throw new Error("No signer connected");
+
+ addLog("info", `Opening channel with ${peerId.slice(0, 20)}…`);
+
+ const addr = await signer.getRecommendedAddressObj();
+ const lock = addr.script;
+ const lockRpc: CkbRpcScript = {
+ code_hash: ccc.hexFrom(lock.codeHash),
+ hash_type: lock.hashType,
+ args: ccc.hexFrom(lock.args),
+ };
+
+ const lockCellDeps = await resolveLockCellDeps(signer.client, lock);
+
+ const openResult = await fiberRef.current!.openChannelWithExternalFunding(
+ {
+ pubkey: peerId,
+ funding_amount: ccc.numToHex(ccc.fixedPointFrom(fundingAmount, 8)),
+ public: isPublic,
+ shutdown_script: lockRpc,
+ funding_lock_script: lockRpc,
+ funding_lock_script_cell_deps:
+ lockCellDeps.length > 0 ? lockCellDeps : undefined,
+ },
+ );
+ logResponse("open_channel_with_external_funding", {
+ channel_id: openResult.channel_id,
+ });
+
+ // Convert unsigned CKB JSON-RPC tx → ccc.Transaction
+ const rpc = openResult.unsigned_funding_tx;
+ const ccTx = ccc.Transaction.from({
+ version: rpc.version,
+ cellDeps: rpc.cell_deps.map((d) => ({
+ depType: d.dep_type === "dep_group" ? "depGroup" : "code",
+ outPoint: { txHash: d.out_point.tx_hash, index: d.out_point.index },
+ })),
+ headerDeps: rpc.header_deps,
+ inputs: rpc.inputs.map((i) => ({
+ previousOutput: {
+ txHash: i.previous_output.tx_hash,
+ index: i.previous_output.index,
+ },
+ since: i.since,
+ })),
+ outputs: rpc.outputs.map((o) => ({
+ capacity: o.capacity,
+ lock: {
+ codeHash: o.lock.code_hash,
+ hashType: o.lock.hash_type,
+ args: o.lock.args,
+ },
+ type: o.type
+ ? {
+ codeHash: o.type.code_hash,
+ hashType: o.type.hash_type,
+ args: o.type.args,
+ }
+ : undefined,
+ })),
+ outputsData: rpc.outputs_data,
+ witnesses: rpc.witnesses,
+ });
+
+ // Populate live-cell data so the signer can compute the signing hash
+ for (const input of ccTx.inputs) {
+ const cell = await signer.client.getCell(input.previousOutput);
+ if (cell) {
+ input.cellOutput = cell.cellOutput;
+ input.outputData = cell.outputData;
+ }
+ }
+
+ addLog("info", "Signing funding transaction…");
+ const signedTx = await signer.signOnlyTransaction(ccTx);
+
+ // Convert signed ccc.Transaction back to CKB JSON-RPC format
+ const signedRpc: CkbRpcTransaction = {
+ version: ccc.numToHex(signedTx.version),
+ cell_deps: signedTx.cellDeps.map((d) => ({
+ dep_type: d.depType === "depGroup" ? "dep_group" : "code",
+ out_point: {
+ tx_hash: ccc.hexFrom(d.outPoint.txHash),
+ index: ccc.numToHex(d.outPoint.index),
+ },
+ })),
+ header_deps: signedTx.headerDeps.map((h) => ccc.hexFrom(h)),
+ inputs: signedTx.inputs.map((i) => ({
+ previous_output: {
+ tx_hash: ccc.hexFrom(i.previousOutput.txHash),
+ index: ccc.numToHex(i.previousOutput.index),
+ },
+ since: ccc.numToHex(i.since),
+ })),
+ outputs: signedTx.outputs.map((o) => ({
+ capacity: ccc.numToHex(o.capacity),
+ lock: {
+ code_hash: ccc.hexFrom(o.lock.codeHash),
+ hash_type: o.lock.hashType,
+ args: ccc.hexFrom(o.lock.args),
+ },
+ type: o.type
+ ? {
+ code_hash: ccc.hexFrom(o.type.codeHash),
+ hash_type: o.type.hashType,
+ args: ccc.hexFrom(o.type.args),
+ }
+ : undefined,
+ })),
+ outputs_data: signedTx.outputsData.map((d) => ccc.hexFrom(d)),
+ witnesses: signedTx.witnesses.map((w) => ccc.hexFrom(w)),
+ };
+
+ addLog("info", "Submitting signed funding transaction…");
+ const submitResult = await fiberRef.current!.submitSignedFundingTx({
+ channel_id: openResult.channel_id,
+ signed_funding_tx: signedRpc,
+ });
+ logResponse("submit_signed_funding_tx", submitResult);
+ return { channel_id: submitResult.channel_id };
+ },
+ [signer, addLog, logResponse],
+ );
+
+ const shutdownChannel = useCallback(
+ async (channelId: string) => {
+ addLog("info", `Closing channel ${channelId.slice(0, 18)}…`);
+ const result = await invoke("shutdown_channel", [
+ { channel_id: channelId },
+ ]);
+ logResponse("shutdown_channel", result);
+ },
+ [addLog, logResponse],
+ );
+
+ const newInvoice = useCallback(
+ async (amount: string, description: string): Promise => {
+ addLog("info", `Creating invoice for ${amount} CKB…`);
+ const preimage = ccc.hexFrom(crypto.getRandomValues(new Uint8Array(32)));
+ const result = await invoke("new_invoice", [
+ {
+ amount: ccc.numToHex(ccc.fixedPointFrom(amount, 8)),
+ currency: "Fibt",
+ payment_preimage: preimage,
+ description: description || undefined,
+ expiry: ccc.numToHex(3600),
+ final_expiry_delta: ccc.numToHex(9600000),
+ },
+ ]);
+ logResponse("new_invoice", result);
+ return result;
+ },
+ [addLog, logResponse],
+ );
+
+ const getInvoice = useCallback(
+ async (paymentHash: string): Promise => {
+ addLog("info", `Checking invoice ${paymentHash.slice(0, 18)}…`);
+ const result = await invoke("get_invoice", [
+ { payment_hash: paymentHash },
+ ]);
+ logResponse("get_invoice", result);
+ return result;
+ },
+ [addLog, logResponse],
+ );
+
+ const sendPayment = useCallback(
+ async (invoice: string): Promise => {
+ addLog("info", "Sending payment…");
+ const result = await invoke("send_payment", [{ invoice }]);
+ logResponse("send_payment", result);
+ return result;
+ },
+ [addLog, logResponse],
+ );
+
+ return {
+ isRunning,
+ isStarting,
+ isRefreshing,
+ nodeInfo,
+ peers,
+ channels,
+ startNode,
+ stopNode,
+ clearNodeData,
+ refreshNodeData,
+ connectPeer,
+ disconnectPeer,
+ openChannel,
+ shutdownChannel,
+ newInvoice,
+ getInvoice,
+ sendPayment,
+ };
+}
diff --git a/packages/demo/src/app/connected/(tools)/Fiber/page.tsx b/packages/demo/src/app/connected/(tools)/Fiber/page.tsx
new file mode 100644
index 000000000..f47283369
--- /dev/null
+++ b/packages/demo/src/app/connected/(tools)/Fiber/page.tsx
@@ -0,0 +1,334 @@
+"use client";
+
+import { Button } from "@/src/components/Button";
+import { TextInput } from "@/src/components/Input";
+import { useApp } from "@/src/context";
+import {
+ ChevronDown,
+ ChevronUp,
+ Copy,
+ HelpCircle,
+ Key,
+ Play,
+ RefreshCw,
+ Settings,
+ Square,
+ Trash2,
+ Wifi,
+ WifiOff,
+} from "lucide-react";
+import { useMemo, useState } from "react";
+import {
+ Card,
+ ChannelsTab,
+ InvoicesTab,
+ LogPanel,
+ NodeInfoGrid,
+ PaymentsTab,
+ PeersTab,
+ TabBar,
+} from "./components";
+import {
+ LS_MANUAL_CONFIG,
+ maskKey,
+ readLs,
+ SIGN_MESSAGE,
+ writeLs,
+} from "./config";
+import { useActivityLog, useFiberNode, useNodeKey } from "./hooks";
+import type { Tab } from "./types";
+
+export default function FiberPage() {
+ const { signer, createSender } = useApp();
+ const { log, error } = createSender("Fiber");
+
+ // ── Config (persisted) ───────────────────────────────────────────────────────
+ const [manualConfig, setManualConfig] = useState(() =>
+ readLs(LS_MANUAL_CONFIG, ""),
+ );
+ const [configOpen, setConfigOpen] = useState(true);
+ const [activeTab, setActiveTab] = useState("peers");
+
+ // ── Signing message (editable by the user) ───────────────────────────────────
+ const [signMessage, setSignMessage] = useState(SIGN_MESSAGE);
+
+ // ── Hooks ────────────────────────────────────────────────────────────────────
+ const { logs, addLog, clearLogs } = useActivityLog();
+ const { walletAddr, storedKey, deriveKeys, keysFromStored } = useNodeKey(
+ signer,
+ addLog,
+ );
+ const node = useFiberNode(signer, addLog);
+
+ // ── Derived values ───────────────────────────────────────────────────────────
+ const dbPrefix = useMemo(
+ () => `/fiber-demo-${walletAddr.slice(0, 12)}`,
+ [walletAddr],
+ );
+
+ // ── Handlers ─────────────────────────────────────────────────────────────────
+ const handleStart = async () => {
+ const fiberKey = keysFromStored() ?? (await deriveKeys(signMessage));
+ if (!fiberKey) {
+ return;
+ }
+ try {
+ await node.startNode({
+ fiberKey,
+ dbPrefix,
+ configYaml: manualConfig || undefined,
+ });
+ log("Fiber node started");
+ } catch {
+ error("Fiber node failed to start");
+ }
+ };
+
+ const handleStop = async () => {
+ await node.stopNode();
+ log("Fiber node stopped");
+ };
+
+ // ── Render ───────────────────────────────────────────────────────────────────
+ return (
+
+ {/* Config */}
+
+ setConfigOpen((v) => !v)}
+ >
+
+ Node Configuration
+
+ {configOpen ? (
+
+ ) : (
+
+ )}
+
+ {configOpen && (
+
+ )}
+
+
+ {/* Identity key — overflow-visible so the signing-message tooltip can
+ escape the card boundary upward without being clipped */}
+ {signer && (
+
+
+
+ Node Identity Key
+
+
+ {/* Editable signing message.
+ The HelpCircle is overlaid after the label text via an
+ invisible ghost span so TextInput keeps label as string. */}
+
+
+
+ Signing Message
+
+
+
+ Your wallet signs this text once to deterministically derive
+ a secp256k1 private key:
+
+
+ fiberKey
+ {" = hashCkb(signature)"} — Fiber P2P identity
+
+
+ CKB channel funding transactions are signed by your
+ connected wallet directly via CCC. The same message always
+ produces the same node identity. Changing it gives you a
+ completely different node identity.
+
+
+
+
+
+
+ {storedKey && (
+
+
+
+ {maskKey(storedKey)}
+
+ {
+ navigator.clipboard.writeText(storedKey);
+ addLog("info", "Key copied.");
+ }}
+ >
+
+
+
+
+ )}
+
+
+ deriveKeys(signMessage)}
+ >
+ {storedKey ? "Re-derive Key" : "Sign & Derive Key"}
+
+
+
+
+ )}
+
+ {/* Node runtime */}
+
+
+
+
+ {node.isRunning ? (
+
+ ) : (
+
+ )}
+ Fiber Node
+
+ {node.isRunning ? "Running" : "Stopped"}
+
+ {node.nodeInfo?.version && (
+
+ v{node.nodeInfo.version}
+
+ )}
+
+
+ {node.isRunning && (
+
+
+ Refresh
+
+ )}
+ {node.isRunning ? (
+
+ Stop
+
+ ) : (
+
+
+ {node.isStarting ? "Starting…" : "Start"}
+
+ )}
+
node.clearNodeData(dbPrefix)}
+ title="Delete all local IndexedDB data for this node and start fresh"
+ >
+ Clear Data
+
+
+
+ {node.nodeInfo && (
+
+ )}
+
+
+
+ {/* Tabbed operations */}
+ {node.isRunning && (
+
+
+
+ {activeTab === "peers" && (
+
+ )}
+ {activeTab === "channels" && (
+
+ node
+ .openChannel(peerId, amount, isPublic)
+ .then(() => undefined)
+ }
+ onClose={node.shutdownChannel}
+ />
+ )}
+ {activeTab === "invoices" && (
+
+ )}
+ {activeTab === "payments" && (
+ node.sendPayment(inv).then(() => undefined)}
+ />
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/demo/src/app/connected/(tools)/Fiber/types.ts b/packages/demo/src/app/connected/(tools)/Fiber/types.ts
new file mode 100644
index 000000000..191fcf2a6
--- /dev/null
+++ b/packages/demo/src/app/connected/(tools)/Fiber/types.ts
@@ -0,0 +1,98 @@
+// ── fiber-js snake_case wire types (subset used by this demo) ─────────────────
+// Defined locally to avoid depending on @nervosnetwork/fiber-js TS declarations.
+
+export type FjNodeInfo = {
+ version: string;
+ pubkey: string;
+ addresses: string[];
+ channel_count: string; // 0x-prefixed hex
+ pending_channel_count: string;
+ peers_count: string;
+};
+
+export type FjPeer = { pubkey: string; address: string };
+
+export type FjChannel = {
+ channel_id: string;
+ pubkey: string;
+ state: { state_name: string; state_flags: string };
+ local_balance: string; // shannons, hex
+ remote_balance: string;
+ is_public: boolean;
+};
+
+export type FjInvoice = {
+ invoice_address: string;
+ invoice: { currency: string; data: { payment_hash: string } };
+};
+
+export type FjGetInvoice = FjInvoice & { status: string };
+
+export type FjPayment = {
+ payment_hash: string;
+ status: string;
+ failed_error?: string;
+};
+
+export type FjOpenChannel = {
+ channel_id: string;
+};
+
+// ── CKB JSON-RPC transaction format (used by external-funding flow) ───────────
+
+export type CkbRpcScript = {
+ code_hash: string;
+ hash_type: string;
+ args: string;
+};
+
+export type CkbRpcTransaction = {
+ version: string;
+ cell_deps: Array<{
+ dep_type: string;
+ out_point: { tx_hash: string; index: string };
+ }>;
+ header_deps: string[];
+ inputs: Array<{
+ previous_output: { tx_hash: string; index: string };
+ since: string;
+ }>;
+ outputs: Array<{ capacity: string; lock: CkbRpcScript; type?: CkbRpcScript }>;
+ outputs_data: string[];
+ witnesses: string[];
+};
+
+// ── Minimal Fiber instance interface (satisfies fiber-js Fiber class) ─────────
+
+export type FiberInstance = {
+ invokeCommand(name: string, args?: unknown[]): Promise;
+ stop(): Promise;
+ openChannelWithExternalFunding(params: {
+ pubkey: string;
+ funding_amount: string;
+ public?: boolean;
+ shutdown_script: CkbRpcScript;
+ funding_lock_script: CkbRpcScript;
+ funding_lock_script_cell_deps?: Array<{
+ dep_type: string;
+ out_point: { tx_hash: string; index: string };
+ }>;
+ }): Promise<{ channel_id: string; unsigned_funding_tx: CkbRpcTransaction }>;
+ submitSignedFundingTx(params: {
+ channel_id: string;
+ signed_funding_tx: CkbRpcTransaction;
+ }): Promise<{ channel_id: string; funding_tx_hash: string }>;
+};
+
+// ── UI types ──────────────────────────────────────────────────────────────────
+
+export type LogLevel = "info" | "warn" | "error" | "success";
+
+export type LogEntry = {
+ id: number;
+ level: LogLevel;
+ time: string;
+ msg: string;
+};
+
+export type Tab = "peers" | "channels" | "invoices" | "payments";
diff --git a/packages/demo/src/app/connected/page.tsx b/packages/demo/src/app/connected/page.tsx
index fd3facab4..033c812cd 100644
--- a/packages/demo/src/app/connected/page.tsx
+++ b/packages/demo/src/app/connected/page.tsx
@@ -48,6 +48,7 @@ const TABS: [ReactNode, string, keyof typeof icons, string][] = [
["Nervos DAO", "/connected/NervosDao", "Vault", "text-pink-500"],
["Dep Group", "/utils/DepGroup", "Boxes", "text-amber-500"],
["SSRI", "/connected/SSRI", "Pill", "text-blue-500"],
+ ["Fiber", "/connected/Fiber", "Zap", "text-purple-500"],
["Hash", "/utils/Hash", "Barcode", "text-violet-500"],
["Mnemonic", "/utils/Mnemonic", "SquareAsterisk", "text-fuchsia-500"],
["Keystore", "/utils/Keystore", "Notebook", "text-rose-500"],
diff --git a/packages/demo/vercel.json b/packages/demo/vercel.json
new file mode 100644
index 000000000..d111d3c78
--- /dev/null
+++ b/packages/demo/vercel.json
@@ -0,0 +1,11 @@
+{
+ "headers": [
+ {
+ "source": "/connected/Fiber",
+ "headers": [
+ { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
+ { "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }
+ ]
+ }
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d52d483a7..7d01d939a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -355,6 +355,9 @@ importers:
'@heroicons/react':
specifier: ^2.2.0
version: 2.2.0(react@19.2.3)
+ '@nervosnetwork/fiber-js':
+ specifier: 0.8.1
+ version: 0.8.1
'@scure/bip32':
specifier: ^2.0.0
version: 2.0.0
@@ -3425,6 +3428,9 @@ packages:
'@nervosnetwork/ckb-types@0.109.5':
resolution: {integrity: sha512-5jQNjFw76YCd+Ppl+0RvBWzxwvWaKfWC5wjVFFdNAieX7xksCHfZFIeow8je7AF8uVypwe56WlLBlblxw9NBBQ==}
+ '@nervosnetwork/fiber-js@0.8.1':
+ resolution: {integrity: sha512-T37qCsvrcVGeX1fjv6FrgwhfwiWPqvVmJfil5tirdYevvoZCcfigqrknMdl1lDar4l1HypDswD4szbD0b3Nz/w==}
+
'@nestjs/cli@11.0.10':
resolution: {integrity: sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==}
engines: {node: '>= 20.11'}
@@ -4885,6 +4891,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
+ async-mutex@0.5.0:
+ resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
+
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -6539,16 +6548,18 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@11.0.3:
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
global-dirs@3.0.1:
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
@@ -9588,6 +9599,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
+ stream-browserify@3.0.0:
+ resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
+
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@@ -9770,6 +9784,7 @@ packages:
tar@7.4.3:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
+ deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
term-size@2.2.1:
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
@@ -13849,6 +13864,11 @@ snapshots:
'@nervosnetwork/ckb-types@0.109.5': {}
+ '@nervosnetwork/fiber-js@0.8.1':
+ dependencies:
+ async-mutex: 0.5.0
+ stream-browserify: 3.0.0
+
'@nestjs/cli@11.0.10(@types/node@24.3.0)':
dependencies:
'@angular-devkit/core': 19.2.15(chokidar@4.0.3)
@@ -15451,6 +15471,10 @@ snapshots:
async-function@1.0.0: {}
+ async-mutex@0.5.0:
+ dependencies:
+ tslib: 2.8.1
+
asynckit@0.4.0: {}
autoprefixer@10.4.21(postcss@8.5.6):
@@ -16733,8 +16757,8 @@ snapshots:
'@next/eslint-plugin-next': 16.0.10
eslint: 9.34.0(jiti@2.5.1)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.34.0(jiti@2.5.1))
eslint-plugin-react: 7.37.5(eslint@9.34.0(jiti@2.5.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.34.0(jiti@2.5.1))
@@ -16769,21 +16793,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)):
- dependencies:
- '@nolyfill/is-core-module': 1.0.39
- debug: 4.4.1
- eslint: 9.34.0(jiti@2.5.1)
- get-tsconfig: 4.10.1
- is-bun-module: 2.0.0
- stable-hash: 0.0.5
- tinyglobby: 0.2.14
- unrs-resolver: 1.11.1
- optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))
- transitivePeerDependencies:
- - supports-color
-
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
@@ -16795,7 +16804,7 @@ snapshots:
tinyglobby: 0.2.14
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))
transitivePeerDependencies:
- supports-color
@@ -16810,14 +16819,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)
eslint: 9.34.0(jiti@2.5.1)
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.5.1))
transitivePeerDependencies:
- supports-color
@@ -16850,7 +16859,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -16861,7 +16870,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.34.0(jiti@2.5.1)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.5.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -21324,6 +21333,11 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
+ stream-browserify@3.0.0:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
streamsearch@1.1.0: {}
string-length@4.0.2:
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 000000000..5423dfd7a
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,14 @@
+{
+ "installCommand": "pnpm install",
+ "buildCommand": "pnpm build && pnpm -C packages/demo build",
+ "outputDirectory": "packages/demo/.next",
+ "headers": [
+ {
+ "source": "/connected/Fiber",
+ "headers": [
+ { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
+ { "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }
+ ]
+ }
+ ]
+}