@livo-build/kit 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./provider";
3
3
  export * from "./contracts";
4
4
  export * from "./tx";
5
5
  export * from "./auth";
6
+ export * from "./verify";
6
7
  export * from "./toast";
7
8
  export * from "./data";
8
9
  export * from "./web3";
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // contracts/ — useContractValue, createLivoContracts (bound to Livo's generated bindings)
8
8
  // tx/ — useTx, TxButton, TxProgress, decodeRevert
9
9
  // auth/ — useSiwe, SignInWithEthereum (Sign-In With Ethereum vs a same-origin /api/auth)
10
+ // verify/ — useWalletVerify, WalletVerifyButton (prove wallet ownership by signing an
11
+ // app-specific message → POST to a backend; the frontend half of runtime Gatekeeper)
10
12
  // toast/ — ToastProvider, useToast
11
13
  // data/ — useApi, useSubgraph, useCollection + DataTable (search/sort/paginate), useEventSource/useWebSocket (realtime)
12
14
  // web3/ — Address, Balance, NetworkGuard, TokenAmount(Input)
@@ -30,6 +32,7 @@ export * from "./provider";
30
32
  export * from "./contracts";
31
33
  export * from "./tx";
32
34
  export * from "./auth";
35
+ export * from "./verify";
33
36
  export * from "./toast";
34
37
  export * from "./data";
35
38
  export * from "./web3";
@@ -0,0 +1,17 @@
1
+ import "../wallet/wallet.css";
2
+ import { type ReactNode } from "react";
3
+ import { type UseWalletVerifyOptions, type WalletVerifyState } from "./useWalletVerify";
4
+ export interface WalletVerifyButtonProps extends UseWalletVerifyOptions {
5
+ className?: string;
6
+ /** Override the button copy per state. */
7
+ labels?: {
8
+ idle?: string;
9
+ signing?: string;
10
+ verifying?: string;
11
+ verified?: string;
12
+ error?: string;
13
+ };
14
+ /** Custom render — receives the full hook state. */
15
+ children?: (api: WalletVerifyState) => ReactNode;
16
+ }
17
+ export declare function WalletVerifyButton({ className, labels, children, ...opts }: WalletVerifyButtonProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import "../wallet/wallet.css";
3
+ import {} from "react";
4
+ import { useWalletVerify } from "./useWalletVerify";
5
+ import { ConnectWallet } from "../wallet";
6
+ import { useIsConnected } from "../account";
7
+ import { cx } from "../util";
8
+ // Drop-in: prompts a wallet connect if needed, then runs connect → sign → verify and
9
+ // reflects the result. Reuses the wallet-connect button styling (wm-cta / wm-account).
10
+ // Pair with the runtime `Gatekeeper` (set `endpoint` to your verifyAndLink route).
11
+ export function WalletVerifyButton({ className, labels, children, ...opts }) {
12
+ const v = useWalletVerify(opts);
13
+ const connected = useIsConnected();
14
+ if (children)
15
+ return _jsx(_Fragment, { children: children(v) });
16
+ // Not connected yet — let the user connect first (the proof needs a wallet).
17
+ if (!connected)
18
+ return _jsx(ConnectWallet, {});
19
+ if (v.status === "verified") {
20
+ return (_jsx("button", { className: cx("wm-account", className), disabled: true, children: labels?.verified ?? "Verified ✓" }));
21
+ }
22
+ const busy = v.status === "signing" || v.status === "verifying";
23
+ const label = v.status === "signing"
24
+ ? labels?.signing ?? "Check your wallet…"
25
+ : v.status === "verifying"
26
+ ? labels?.verifying ?? "Verifying…"
27
+ : v.status === "error"
28
+ ? labels?.error ?? "Try again"
29
+ : labels?.idle ?? "Verify wallet";
30
+ return (_jsx("button", { className: cx("wm-cta", className), disabled: busy, onClick: () => (v.status === "error" ? v.reset() : void v.verify()), children: label }));
31
+ }
@@ -0,0 +1,2 @@
1
+ export { useWalletVerify, buildVerifyMessage, isVerifySuccess, DEFAULT_VERIFY_MESSAGE_PREFIX, type UseWalletVerifyOptions, type WalletVerifyState, type WalletVerifyStatus, type WalletVerifyResult, } from "./useWalletVerify";
2
+ export { WalletVerifyButton, type WalletVerifyButtonProps } from "./WalletVerifyButton";
@@ -0,0 +1,2 @@
1
+ export { useWalletVerify, buildVerifyMessage, isVerifySuccess, DEFAULT_VERIFY_MESSAGE_PREFIX, } from "./useWalletVerify";
2
+ export { WalletVerifyButton } from "./WalletVerifyButton";
@@ -0,0 +1,68 @@
1
+ export type WalletVerifyStatus = "idle" | "signing" | "verifying" | "verified" | "error";
2
+ /** Whatever the backend returns from the verify route (ok + any extra fields). */
3
+ export interface WalletVerifyResult {
4
+ ok: boolean;
5
+ address?: string;
6
+ [key: string]: unknown;
7
+ }
8
+ export interface UseWalletVerifyOptions {
9
+ /**
10
+ * POST endpoint that verifies the signature + records the proof (default
11
+ * "/api/verify"). Mirrors the backend `Gatekeeper.verifyAndLink` route — it
12
+ * receives `{ nonce, address, signature }` and returns `{ ok, ... }`. When
13
+ * pairing with the `wallet-link-bot` template (which serves it at `/link`), set
14
+ * `endpoint: "/link"`.
15
+ */
16
+ endpoint?: string;
17
+ /**
18
+ * The challenge nonce. Defaults to the `?n=` query param — the backend
19
+ * `Gatekeeper.challenge()` embeds it in the link URL it DMs the user. Pass it
20
+ * explicitly, or use `nonceEndpoint` to have the frontend mint one.
21
+ */
22
+ nonce?: string;
23
+ /** GET endpoint that mints a nonce (and optionally the exact message) → `{ nonce, message? }`. */
24
+ nonceEndpoint?: string;
25
+ /**
26
+ * Prefix prepended to the nonce to form the signed message. MUST match the
27
+ * backend `Gatekeeper`'s `messagePrefix` byte-for-byte. Default matches the
28
+ * Gatekeeper default — override on BOTH sides to make the text app-specific.
29
+ */
30
+ messagePrefix?: string;
31
+ /** Full control of the signed text (overrides messagePrefix). A server-supplied message wins. */
32
+ message?: string | ((address: string, nonce: string) => string);
33
+ /** Send cookies with the verify POST (for a session-issuing backend). */
34
+ credentials?: boolean;
35
+ onVerified?: (result: WalletVerifyResult) => void;
36
+ onError?: (error: Error) => void;
37
+ }
38
+ export interface WalletVerifyState {
39
+ status: WalletVerifyStatus;
40
+ /** The connected wallet address (the one being proven). */
41
+ address?: string;
42
+ /** The nonce used for the current/last attempt. */
43
+ nonce?: string;
44
+ /** The backend's response on success. */
45
+ result?: WalletVerifyResult;
46
+ /** Connect-aware: false until a wallet is connected. */
47
+ ready: boolean;
48
+ verify: () => Promise<void>;
49
+ reset: () => void;
50
+ error?: Error;
51
+ }
52
+ /** The default signed-message prefix — matches the runtime `Gatekeeper` default. */
53
+ export declare const DEFAULT_VERIFY_MESSAGE_PREFIX = "Link my wallet to this community \u00B7 nonce: ";
54
+ /**
55
+ * Build the exact text to sign. A server-supplied message wins (guaranteed match);
56
+ * otherwise `message` (string or fn), otherwise `messagePrefix + nonce`. Pure —
57
+ * exported for testing and so callers can preview the message.
58
+ */
59
+ export declare function buildVerifyMessage(opts: Pick<UseWalletVerifyOptions, "message" | "messagePrefix">, address: string, nonce: string, serverMessage?: string): string;
60
+ /** Read success out of a verify response, accepting ok/verified/linked. Pure — tested. */
61
+ export declare function isVerifySuccess(httpOk: boolean, json: Record<string, unknown>): boolean;
62
+ /**
63
+ * Prove the connected wallet owns an address by signing an app-specific message and
64
+ * POSTing the proof to your backend (the frontend half of the runtime `Gatekeeper`:
65
+ * connect → sign(messagePrefix + nonce) → verifyAndLink). No gas, no transaction.
66
+ * Use `<WalletVerifyButton>` for a drop-in button, or this hook for custom UI.
67
+ */
68
+ export declare function useWalletVerify(options?: UseWalletVerifyOptions): WalletVerifyState;
@@ -0,0 +1,104 @@
1
+ import { useCallback, useState } from "react";
2
+ import { useConnection, useSignMessage } from "wagmi";
3
+ /** The default signed-message prefix — matches the runtime `Gatekeeper` default. */
4
+ export const DEFAULT_VERIFY_MESSAGE_PREFIX = "Link my wallet to this community · nonce: ";
5
+ /** Read the `?n=` nonce from the page URL (browser only). */
6
+ function nonceFromUrl() {
7
+ if (typeof window === "undefined")
8
+ return undefined;
9
+ return new URLSearchParams(window.location.search).get("n") ?? undefined;
10
+ }
11
+ /**
12
+ * Build the exact text to sign. A server-supplied message wins (guaranteed match);
13
+ * otherwise `message` (string or fn), otherwise `messagePrefix + nonce`. Pure —
14
+ * exported for testing and so callers can preview the message.
15
+ */
16
+ export function buildVerifyMessage(opts, address, nonce, serverMessage) {
17
+ if (serverMessage)
18
+ return serverMessage;
19
+ if (typeof opts.message === "function")
20
+ return opts.message(address, nonce);
21
+ if (typeof opts.message === "string")
22
+ return opts.message;
23
+ return (opts.messagePrefix ?? DEFAULT_VERIFY_MESSAGE_PREFIX) + nonce;
24
+ }
25
+ /** Read success out of a verify response, accepting ok/verified/linked. Pure — tested. */
26
+ export function isVerifySuccess(httpOk, json) {
27
+ return httpOk && (json.ok === true || json.verified === true || json.linked === true);
28
+ }
29
+ /**
30
+ * Prove the connected wallet owns an address by signing an app-specific message and
31
+ * POSTing the proof to your backend (the frontend half of the runtime `Gatekeeper`:
32
+ * connect → sign(messagePrefix + nonce) → verifyAndLink). No gas, no transaction.
33
+ * Use `<WalletVerifyButton>` for a drop-in button, or this hook for custom UI.
34
+ */
35
+ export function useWalletVerify(options = {}) {
36
+ const base = options.endpoint ?? "/api/verify";
37
+ const { address } = useConnection();
38
+ const { signMessageAsync } = useSignMessage();
39
+ const [status, setStatus] = useState("idle");
40
+ const [nonce, setNonce] = useState();
41
+ const [result, setResult] = useState();
42
+ const [error, setError] = useState();
43
+ const verify = useCallback(async () => {
44
+ if (!address) {
45
+ const e = new Error("Connect a wallet first.");
46
+ setError(e);
47
+ setStatus("error");
48
+ options.onError?.(e);
49
+ return;
50
+ }
51
+ setStatus("signing");
52
+ setError(undefined);
53
+ try {
54
+ // 1) Resolve the nonce: explicit option → ?n= link → mint from nonceEndpoint.
55
+ let n = options.nonce ?? nonceFromUrl();
56
+ let serverMessage;
57
+ if (!n && options.nonceEndpoint) {
58
+ const nr = await fetch(`${options.nonceEndpoint}?address=${address}`, {
59
+ credentials: options.credentials ? "include" : "same-origin",
60
+ });
61
+ const nj = (await nr.json());
62
+ n = nj.nonce;
63
+ serverMessage = nj.message;
64
+ }
65
+ if (!n) {
66
+ throw new Error("No challenge nonce — pass `nonce`, open via a `?n=` link, or set `nonceEndpoint`.");
67
+ }
68
+ setNonce(n);
69
+ // 2) Sign the app-specific message (must match the backend's reconstruction).
70
+ const msg = buildVerifyMessage(options, address, n, serverMessage);
71
+ const signature = await signMessageAsync({ message: msg });
72
+ // 3) POST the proof to the backend (Gatekeeper.verifyAndLink).
73
+ setStatus("verifying");
74
+ const res = await fetch(base, {
75
+ method: "POST",
76
+ credentials: options.credentials ? "include" : "same-origin",
77
+ headers: { "content-type": "application/json" },
78
+ body: JSON.stringify({ nonce: n, address, signature }),
79
+ });
80
+ const j = (await res.json().catch(() => ({})));
81
+ if (!isVerifySuccess(res.ok, j)) {
82
+ throw new Error(j.error ?? "Verification failed");
83
+ }
84
+ const out = { ok: true, address, ...j };
85
+ setResult(out);
86
+ setStatus("verified");
87
+ options.onVerified?.(out);
88
+ }
89
+ catch (e) {
90
+ setError(e);
91
+ setStatus("error");
92
+ options.onError?.(e);
93
+ }
94
+ // options is intentionally read fresh each call (callbacks/config may change).
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, [address, base, signMessageAsync]);
97
+ const reset = useCallback(() => {
98
+ setStatus("idle");
99
+ setNonce(undefined);
100
+ setResult(undefined);
101
+ setError(undefined);
102
+ }, []);
103
+ return { status, address, nonce, result, ready: Boolean(address), verify, reset, error };
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/kit",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Livo frontend kit — reusable React + wagmi v3 building blocks (wallet connect modal, providers, hooks) for web3 apps built on Livo.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",