Skip to content
create-bsv-app

Client API

Every function and component the scaffold writes to client/src/bsv/, with its exact signature, return value, parameters and errors.

These live in your project, in client/src/bsv/ (or src/bsv/ for the react starter). They're plain TypeScript you own, so open them, change them, or delete what you don't use. The source of each one is at the end of its section, exactly as create-bsv-app 1.1.2 generates it (with the build fix applied).

Hover any underlined name in the examples for its real type.

useWallet()#

The connected wallet, the user's identity key, and the connect flow. From wallet-connect.

ts
import { useWallet } from './bsv/WalletContext'

Usage#

client/src/Profile.tsx
import { function useWallet(): WalletStateuseWallet } from './bsv/WalletContext'

export function function Profile(): React.JSX.ElementProfile () {
  const { const status: ConnectStatusstatus, const identityKey: string | nullidentityKey, const connect: () => Promise<void>connect } = function useWallet(): WalletStateuseWallet()
  if (const status: ConnectStatusstatus !== 'connected') return <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => void const connect: () => Promise<void>connect()}>Connect wallet</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
  return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Signed in as {const identityKey: string | nullidentityKey}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
}

Returns#

An object, re-rendered whenever the connection changes:

Field Type Meaning
status ConnectStatus 'disconnected', 'connecting', 'choosing', 'pairing' or 'connected'
connected boolean wallet !== null
wallet WalletInterface | null the full BRC-100 wallet from @bsv/sdk
identityKey string | null the user's identity key, 66 hex characters
connect () => Promise<void> try the desktop wallet; on failure, move to 'choosing'
connectMobile () => Promise<void> start QR pairing ('pairing'); needs the server's relay
cancel () => void back to 'disconnected'

Throws#

  • useWallet must be used within WalletProvider if the component isn't inside <WalletProviders>.
Deep diveSource: WalletContext.tsx
client/src/bsv/WalletContext.tsx
// App-wide wallet state + connect state machine (desktop-first, relay fallback).
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
import type { WalletInterface } from '@bsv/sdk'
import { connectDesktopWallet } from './walletAcquisition.js'
import { useWalletConnection } from './WalletConnectionContext.js'

export type ConnectStatus = 'disconnected' | 'connecting' | 'choosing' | 'pairing' | 'connected'
interface WalletState {
  wallet: WalletInterface | null
  identityKey: string | null
  connected: boolean
  status: ConnectStatus
  connect: () => Promise<void>          // desktop-first; on failure -> 'choosing'
  connectMobile: () => Promise<void>    // relay QR -> 'pairing'
  cancel: () => void
}
const Ctx = createContext<WalletState | null>(null)

export function WalletProvider ({ children }: { children: ReactNode }) {
  const relay = useWalletConnection()
  const [wallet, setWallet] = useState<WalletInterface | null>(null)
  const [identityKey, setIdentityKey] = useState<string | null>(null)
  const [status, setStatus] = useState<ConnectStatus>('disconnected')

  const connect = useCallback(async () => {
    setStatus('connecting')
    try {
      const { wallet, identityKey } = await connectDesktopWallet()
      setWallet(wallet); setIdentityKey(identityKey); setStatus('connected')
    } catch {
      setStatus('choosing')   // no desktop wallet -> show modal
    }
  }, [])

  const connectMobile = useCallback(async () => {
    setStatus('pairing')
    try {
      await relay.createSession()   // shows QR via relay.session.qrDataUrl
    } catch {
      setStatus('choosing')         // relay unavailable -> back to the choice modal
    }
  }, [relay])

  const cancel = useCallback(() => { relay.cancelSession?.(); setStatus('disconnected') }, [relay])

  // bridge: when the relay session connects, adopt its wallet
  useEffect(() => {
    if (relay.session?.status === 'connected' && relay.wallet != null && wallet == null) {
      const w = relay.wallet as unknown as WalletInterface
      w.getPublicKey({ identityKey: true }).then(({ publicKey }) => {
        setWallet(w); setIdentityKey(publicKey); setStatus('connected')
      }).catch(() => {})
    }
  }, [relay.session?.status, relay.wallet, wallet])

  return <Ctx.Provider value={{ wallet, identityKey, connected: wallet !== null, status, connect, connectMobile, cancel }}>{children}</Ctx.Provider>
}
export function useWallet (): WalletState {
  const v = useContext(Ctx)
  if (v === null) throw new Error('useWallet must be used within WalletProvider')
  return v
}

WalletProviders#

Wraps your app so every component can call useWallet(). New projects already have it in main.tsx; add mode prints the snippet in AGENTS.md.

client/src/main.tsx
import { function createRoot(container: Container, options?: RootOptions): RootcreateRoot } from 'react-dom/client'
import { 
function WalletProviders({ children }: {
    children: React.ReactNode;
}): React.JSX.Element
WalletProviders
} from './bsv/WalletProviders'
import function App(): nullApp from './App' function createRoot(container: Container, options?: RootOptions): RootcreateRoot(var document: Documentdocument.Document.getElementById(elementId: string): HTMLElement | nullgetElementById('root')!).Root.render(children: React.ReactNode): voidrender( <
function WalletProviders({ children }: {
    children: React.ReactNode;
}): React.JSX.Element
WalletProviders
>
<function App(): nullApp /> </
function WalletProviders({ children }: {
    children: React.ReactNode;
}): React.JSX.Element
WalletProviders
>,
)

It nests the mobile relay provider (WalletConnectionProvider, from @bsv/wallet-relay) above the wallet provider, which is the order useWallet() needs.

Deep diveSource: WalletProviders.tsx and WalletConnectionContext.tsx
// Compose the wallet providers in the required order (relay above wallet).
import './bsv.css'
import type { ReactNode } from 'react'
import { WalletConnectionProvider } from './WalletConnectionContext.js'
import { WalletProvider } from './WalletContext.js'

export function WalletProviders ({ children }: { children: ReactNode }) {
  return (
    <WalletConnectionProvider>
      <WalletProvider>{children}</WalletProvider>
    </WalletConnectionProvider>
  )
}

ConnectWallet#

The ready-made button: Connect wallet, then Connected: 02ab…, with the No desktop wallet found dialog (mobile QR or install link) in between.

tsx
import { function ConnectWallet(): React.JSX.ElementConnectWallet } from './bsv/ConnectWallet'

export const const Header: () => React.JSX.ElementHeader = () => <React.JSX.IntrinsicElements.header: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>header><function ConnectWallet(): React.JSX.ElementConnectWallet /></React.JSX.IntrinsicElements.header: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>header>

It takes no props. Restyle it in bsv.css, or build your own UI on useWallet().

useWalletLogin()#

Passwordless login: sign a login proof and post it to the server. From wallet-login.

ts
import { useWalletLogin } from './bsv/useWalletLogin'

Usage#

client/src/LoginButton.tsx
import { 
function useWalletLogin(opts?: UseWalletLoginOptions): {
    login: () => Promise<{
        identityKey: string;
    }>;
    identityKey: string | null;
    connected: boolean;
}
useWalletLogin
} from './bsv/useWalletLogin'
export function function LoginButton(): React.JSX.ElementLoginButton () { const {
const login: () => Promise<{
    identityKey: string;
}>
login
} =
function useWalletLogin(opts?: UseWalletLoginOptions): {
    login: () => Promise<{
        identityKey: string;
    }>;
    identityKey: string | null;
    connected: boolean;
}
useWalletLogin
()
const const onClick: () => Promise<void>onClick = async () => { const { const identityKey: stringidentityKey } = await
const login: () => Promise<{
    identityKey: string;
}>
login
()
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(const identityKey: stringidentityKey) // → 02a1f3c9e8b7d6a5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3 } return <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => void const onClick: () => Promise<void>onClick()}>Log in with wallet</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> }

Returns#

Field Type Meaning
login () => Promise<{ identityKey: string }> runs the whole exchange and resolves with the verified key
identityKey string | null the connected wallet's key (before login)
connected boolean whether a wallet is connected

Parameters#

One optional object:

serverIdentityKey (optional)#

  • Type: string

Pin the server's identity instead of fetching it from GET /api/identity. See why you might.

loginEndpoint (optional)#

  • Type: string
  • Default: '/api/login'

The path to post the proof to. It must be a plain path: no query strings.

Throws#

  • connect a wallet first (initializeWallet / relay) when no wallet is connected.
  • login failed: <status> when the server rejects the proof (usually 401).
  • server returned another wallet identity when the verified key isn't the connected wallet's.
  • Anything apiFetch or getServerIdentity throws.
Deep diveSource: useWalletLogin.tsx
client/src/bsv/useWalletLogin.tsx
// Wallet login: prove identity with the connected wallet, then POST the proof.
import { useCallback } from 'react'
import { useWallet } from './WalletContext.js'
import { createAuthProof } from './auth.js'
import { getServerIdentity, readIdentityKeyResponse, requireIdentityKey } from './serverIdentity.js'
import { apiFetch } from './apiClient.js'

// serverIdentityKey is optional: when omitted it's fetched from GET /api/identity.
export interface UseWalletLoginOptions { serverIdentityKey?: string, loginEndpoint?: string }

export function useWalletLogin (opts: UseWalletLoginOptions = {}) {
  const { wallet, identityKey } = useWallet()
  const login = useCallback(async (): Promise<{ identityKey: string }> => {
    if (wallet === null) throw new Error('connect a wallet first (initializeWallet / relay)')
    const counterparty = requireIdentityKey(opts.serverIdentityKey ?? await getServerIdentity())
    const proof = await createAuthProof(wallet, { counterparty, action: 'login' })
    const res = await apiFetch(opts.loginEndpoint ?? '/api/login', {
      method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(proof)
    })
    if (!res.ok) throw new Error('login failed: ' + String(res.status))
    const loggedInIdentity = await readIdentityKeyResponse(res)
    if (identityKey !== null && loggedInIdentity !== identityKey) throw new Error('server returned another wallet identity')
    return { identityKey: loggedInIdentity }
  }, [wallet, opts.serverIdentityKey, opts.loginEndpoint])
  return { login, identityKey, connected: wallet !== null }
}

useSignedRequest()#

Authenticate a single API call. From signed-requests.

ts
import { useSignedRequest } from './bsv/useSignedRequest'

Usage#

client/src/NewNote.tsx
import { 
function useSignedRequest(serverIdentityKey?: string): {
    signedFetch: (url: string, opts: {
        action: string;
        body?: RequestBody;
    }) => Promise<Response>;
    connected: boolean;
}
useSignedRequest
} from './bsv/useSignedRequest'
export function function NewNote(): React.JSX.ElementNewNote () { const {
const signedFetch: (url: string, opts: {
    action: string;
    body?: RequestBody;
}) => Promise<Response>
signedFetch
, const connected: booleanconnected } =
function useSignedRequest(serverIdentityKey?: string): {
    signedFetch: (url: string, opts: {
        action: string;
        body?: RequestBody;
    }) => Promise<Response>;
    connected: boolean;
}
useSignedRequest
()
const const save: () => Promise<void>save = async () => { const const res: Responseres = await
const signedFetch: (url: string, opts: {
    action: string;
    body?: RequestBody;
}) => Promise<Response>
signedFetch
('/api/notes', { action: stringaction: 'create-note', body?: RequestBody | undefinedbody: { text: stringtext: 'gm' } })
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(const res: Responseres.Response.status: numberstatus) // → 200 } return <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={!const connected: booleanconnected} React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => void const save: () => Promise<void>save()}>Save</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> }

Returns#

Field Type Meaning
signedFetch (path: string, opts: { action: string, body?: RequestBody }) => Promise<Response> signs { action, body } and POSTs { proof, body } as JSON
connected boolean whether a wallet is connected

Parameters#

serverIdentityKey (optional)#

  • Type: string

The first argument (not an object). Pins the server's identity instead of fetching it.

signedFetch: path#

  • Type: string

A plain API path such as /api/notes. Query strings aren't allowed.

signedFetch: opts.action#

  • Type: string

Names the operation. The server must verify the same action, or the request fails with 401.

signedFetch: opts.body (optional)#

  • Type: RequestBody (a string, binary, or JSON-compatible object or array)

Bound into the signature byte for byte. See why exact bytes matter.

Throws#

  • connect a wallet first when no wallet is connected.
  • Anything apiFetch or getServerIdentity throws. A rejected proof is not thrown: check res.ok / res.status.
Deep diveSource: useSignedRequest.ts and signedRequest.ts
// Hook: signedFetch attaches a proof bound to the route + JSON body.
import { useCallback } from 'react'
import { useWallet } from './WalletContext.js'
import { createSignedRequest } from './signedRequest.js'
import { getServerIdentity, requireIdentityKey } from './serverIdentity.js'
import { apiFetch } from './apiClient.js'
import type { RequestBody } from './auth.js'

// serverIdentityKey is optional: when omitted it's fetched from GET /api/identity.
export function useSignedRequest (serverIdentityKey?: string) {
  const { wallet } = useWallet()
  const signedFetch = useCallback(async (url: string, opts: { action: string, body?: RequestBody }): Promise<Response> => {
    if (wallet === null) throw new Error('connect a wallet first')
    const counterparty = requireIdentityKey(serverIdentityKey ?? await getServerIdentity())
    const proof = await createSignedRequest(wallet, { serverIdentityKey: counterparty, action: opts.action, body: opts.body })
    return await apiFetch(url, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ proof, body: opts.body })
    })
  }, [wallet, serverIdentityKey])
  return { signedFetch, connected: wallet !== null }
}

apiFetch()#

The one HTTP client every generated call uses. Bounded and redirect-free by design: see the security model.

ts
import { apiFetch, readApiJson } from './bsv/apiClient'

Usage#

ts
import { function apiFetch(path: string, init?: RequestInit): Promise<Response>apiFetch, function readApiJson(response: Response): Promise<unknown>readApiJson } from './bsv/apiClient'

const const res: Responseres = await function apiFetch(path: string, init?: RequestInit): Promise<Response>apiFetch('/api/identity')
const const data: unknowndata = await function readApiJson(response: Response): Promise<unknown>readApiJson(const res: Responseres)
var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(const data: unknowndata)
// → { identityKey: '03d234c27a69dfff…' }

Returns#

Promise<Response>: a fresh Response whose body has already been read within the limits, so you can call .json(), .text() or readApiJson on it.

Parameters#

path#

  • Type: string

Must match /^\/[A-Za-z0-9/_-]*$/ and contain no ... It's appended to API_BASE_URL.

init (optional)#

  • Type: RequestInit

Standard fetch options. body must be a string (use JSON.stringify). credentials, redirect, referrerPolicy and signal are always overridden.

Throws#

API endpoint must be a safe absolute path, API request body must be a string, API request exceeds the byte limit, API response exceeds the byte limit, API response has an invalid Content-Length, API response length does not match Content-Length, API response changed network authority, plus AbortError after 10 seconds. All are listed in Errors.

readApiJson()#

Parses a response body as strict UTF-8 JSON.

  • Signature: readApiJson(response: Response): Promise<unknown>
  • Throws: API response is not valid UTF-8, API response is not valid JSON.
Deep diveSource: apiClient.ts
client/src/bsv/apiClient.ts
// One bounded client for every generated API request. It refuses redirects so
// proofs, identities, and future credentials never move to another network authority.
import { API_BASE_URL, API_ORIGIN } from './config.js'

const API_TIMEOUT_MS = 10_000
const MAX_API_REQUEST_BYTES = 1024 * 1024
const MAX_API_RESPONSE_BYTES = 1024 * 1024

function endpointUrl (path: string): string {
  if (!/^\/[A-Za-z0-9/_-]*$/.test(path) || path.includes('..')) {
    throw new TypeError('API endpoint must be a safe absolute path')
  }
  return API_BASE_URL + path
}

function contentLength (response: Response): number | undefined {
  // Fetch exposes decoded response bytes while some implementations retain the
  // encoded Content-Length. The streaming ceiling below remains authoritative.
  if (response.headers.get('content-encoding') !== null) return undefined
  const value = response.headers.get('content-length')
  if (value === null) return undefined
  if (!/^(?:0|[1-9]\d*)$/.test(value)) throw new Error('API response has an invalid Content-Length')
  const length = Number(value)
  if (!Number.isSafeInteger(length)) throw new Error('API response has an invalid Content-Length')
  return length
}

async function readBoundedBody (response: Response): Promise<Uint8Array<ArrayBuffer>> {
  const declared = contentLength(response)
  if (declared !== undefined && declared > MAX_API_RESPONSE_BYTES) {
    throw new Error('API response exceeds the byte limit')
  }
  if (response.body === null) return new Uint8Array()
  const reader = response.body.getReader()
  const chunks: Uint8Array[] = []
  let total = 0
  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      total += value.byteLength
      if (total > MAX_API_RESPONSE_BYTES) {
        await reader.cancel()
        throw new Error('API response exceeds the byte limit')
      }
      chunks.push(value)
    }
  } finally {
    reader.releaseLock()
  }
  if (declared !== undefined && declared !== total) {
    throw new Error('API response length does not match Content-Length')
  }
  const body = new Uint8Array(total)
  let offset = 0
  for (const chunk of chunks) { body.set(chunk, offset); offset += chunk.byteLength }
  return body
}

export async function apiFetch (path: string, init: RequestInit = {}): Promise<Response> {
  if (init.body != null && typeof init.body !== 'string') {
    throw new TypeError('API request body must be a string')
  }
  if (typeof init.body === 'string' && new TextEncoder().encode(init.body).byteLength > MAX_API_REQUEST_BYTES) {
    throw new Error('API request exceeds the byte limit')
  }
  const controller = new AbortController()
  const timer = setTimeout(() => { controller.abort() }, API_TIMEOUT_MS)
  try {
    const response = await fetch(endpointUrl(path), {
      ...init,
      credentials: 'omit',
      redirect: 'error',
      referrerPolicy: 'no-referrer',
      signal: controller.signal
    })
    if (response.redirected || (response.url !== '' && new URL(response.url).origin !== API_ORIGIN)) {
      throw new Error('API response changed network authority')
    }
    const body = await readBoundedBody(response)
    const headers = new Headers(response.headers)
    headers.delete('content-encoding')
    headers.delete('content-length')
    return new Response(body.length === 0 ? null : body, {
      status: response.status,
      statusText: response.statusText,
      headers
    })
  } finally {
    clearTimeout(timer)
  }
}

export async function readApiJson (response: Response): Promise<unknown> {
  const bytes = new Uint8Array(await response.arrayBuffer())
  let text: string
  try {
    text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
  } catch {
    throw new Error('API response is not valid UTF-8')
  }
  try {
    return JSON.parse(text)
  } catch {
    throw new Error('API response is not valid JSON')
  }
}

getServerIdentity()#

Fetches and caches the server's identity key, the counterparty of every proof.

ts
import { function getServerIdentity(endpoint?: string): Promise<string>getServerIdentity } from './bsv/serverIdentity'

const const serverKey: stringserverKey = await function getServerIdentity(endpoint?: string): Promise<string>getServerIdentity()
// → 03d234c27a69dfff14eb8190cfdc1c980c4b31450f9ba412926c0755bfc0d5472f
  • Signature: getServerIdentity(endpoint?: string): Promise<string> (default endpoint '/api/identity')
  • Caching: the first successful result is kept for the life of the page; concurrent calls share one request.
  • Throws: failed to fetch server identity: <status>, server returned an invalid identity response, server returned an invalid identity key.

requireIdentityKey(value) and readIdentityKeyResponse(res) are exported from the same file. They validate a key (compressed 02/03 hex, on the curve) and a strict { identityKey } response.

Deep diveSource: serverIdentity.ts
client/src/bsv/serverIdentity.ts
// Fetch the configured API's identity public key once and cache it.
// This is endpoint/TLS trust, not independent key authentication. Pass a pinned key to
// the login/signed-request hooks when the application requires identity continuity.
import { PublicKey } from '@bsv/sdk'
import { apiFetch, readApiJson } from './apiClient.js'

let cached: string | null = null
let pending: Promise<string> | null = null

export function requireIdentityKey (value: unknown): string {
  if (typeof value !== 'string' || !/^(?:02|03)[0-9a-f]{64}$/.test(value)) {
    throw new Error('server returned an invalid identity key')
  }
  try {
    if (PublicKey.fromString(value).toString() !== value) throw new Error()
  } catch {
    throw new Error('server returned an invalid identity key')
  }
  return value
}

export async function readIdentityKeyResponse (response: Response): Promise<string> {
  const parsed = await readApiJson(response)
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) ||
      (Object.getPrototypeOf(parsed) !== Object.prototype && Object.getPrototypeOf(parsed) !== null) ||
      Object.getOwnPropertySymbols(parsed).length !== 0) {
    throw new Error('server returned an invalid identity response')
  }
  const descriptors = Object.getOwnPropertyDescriptors(parsed)
  if (Object.keys(descriptors).length !== 1 || !Object.prototype.hasOwnProperty.call(descriptors, 'identityKey') ||
      Object.values(descriptors).some(property => property.get != null || property.set != null)) {
    throw new Error('server returned an invalid identity response')
  }
  return requireIdentityKey(descriptors.identityKey?.value)
}

export async function getServerIdentity (endpoint = '/api/identity'): Promise<string> {
  if (cached !== null) return cached
  pending ??= (async () => {
    const res = await apiFetch(endpoint)
    if (!res.ok) throw new Error('failed to fetch server identity: ' + String(res.status))
    const identityKey = await readIdentityKeyResponse(res)
    cached = identityKey
    return identityKey
  })()
  try {
    return await pending
  } finally {
    pending = null
  }
}

Config#

client/src/bsv/config.ts reads the environment once, at import:

Export From Default (dev)
API_BASE_URL VITE_API_URL http://localhost:3000
API_ORIGIN derived from API_BASE_URL http://localhost:3000
BSV_NETWORK VITE_BSV_NETWORK 'test'

In production builds it throws at import without an HTTPS VITE_API_URL. See Environment.

Deep diveSource: config.ts
client/src/bsv/config.ts
// Centralized client configuration. Vite loads VITE_-prefixed vars from client/.env.
// Base URL of the server API. Defaults to the dev server; set VITE_API_URL in production
// (or whenever the client is served from a different origin than the API).
const configuredApiUrl = import.meta.env.VITE_API_URL
if (import.meta.env.PROD && configuredApiUrl == null) {
  throw new Error('VITE_API_URL is required in production')
}
const parsedApiUrl = new URL(configuredApiUrl ?? 'http://localhost:3000')
const localDevelopment = parsedApiUrl.protocol === 'http:' &&
  (parsedApiUrl.hostname === 'localhost' || parsedApiUrl.hostname === '127.0.0.1' || parsedApiUrl.hostname === '[::1]')
if ((parsedApiUrl.protocol !== 'https:' && !localDevelopment) || parsedApiUrl.username !== '' ||
    parsedApiUrl.password !== '' || parsedApiUrl.search !== '' || parsedApiUrl.hash !== '') {
  throw new Error('VITE_API_URL must be credential-free HTTPS (or exact HTTP localhost development)')
}
export const API_BASE_URL = parsedApiUrl.href.replace(/\/$/, '')
export const API_ORIGIN = parsedApiUrl.origin

// The scaffolded network default is concrete and can be overridden per deployment.
const configuredNetwork = import.meta.env.VITE_BSV_NETWORK ?? 'test'
if (configuredNetwork !== 'main' && configuredNetwork !== 'test' && configuredNetwork !== 'ttn') {
  throw new Error('VITE_BSV_NETWORK must be main, test, or ttn')
}
export const BSV_NETWORK = configuredNetwork

createAuthProof()#

The primitive under login and signed requests, shared by client and server (auth.ts). You rarely call it directly; signedFetch and login do. See Server API → verifyAuthProof for the other half.

ts
import { 
function createAuthProof(wallet: ProofSignerWallet, opts: {
    counterparty: string;
    action: string;
    body?: RequestBody;
}): Promise<AuthProof>
createAuthProof
} from './bsv/auth'
const const proof: AuthProofproof = await
function createAuthProof(wallet: ProofSignerWallet, opts: {
    counterparty: string;
    action: string;
    body?: RequestBody;
}): Promise<AuthProof>
createAuthProof
(const wallet: WalletInterfacewallet, { counterparty: stringcounterparty: const serverKey: stringserverKey, action: stringaction: 'login' })
  • Signature: createAuthProof(wallet, { counterparty: string, action: string, body?: RequestBody }): Promise<AuthProof>
  • Returns: { data, signature }: the signed { action, identityKey, expiresAt, nonce } and the signature bytes. Valid for 2 minutes.