@bwalletx/connect 0.4.1 → 0.5.0
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/CHANGELOG.md +9 -0
- package/assets/protocol/401.jpg +0 -0
- package/assets/protocol/401.mp4 +0 -0
- package/assets/protocol/402.jpg +0 -0
- package/assets/protocol/402.mp4 +0 -0
- package/assets/protocol/403.jpg +0 -0
- package/assets/protocol/403.mp4 +0 -0
- package/dist/access-Z6xQwKVF.d.cts +118 -0
- package/dist/access-Z6xQwKVF.d.ts +118 -0
- package/dist/bwalletx-connect.global.js +17 -17
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/{chunk-NNMUSWPG.js → chunk-CVLXFNAG.js} +123 -1
- package/dist/chunk-CVLXFNAG.js.map +1 -0
- package/dist/{chunk-FUQ46RJV.js → chunk-GF6GUDZL.js} +2 -2
- package/dist/chunk-MWD4BCO5.js +182 -0
- package/dist/chunk-MWD4BCO5.js.map +1 -0
- package/dist/{chunk-7NAKOJGZ.js → chunk-QRXFL66W.js} +220 -10
- package/dist/chunk-QRXFL66W.js.map +1 -0
- package/dist/element.cjs +209 -0
- package/dist/element.cjs.map +1 -1
- package/dist/element.js +2 -1
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +292 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +30 -4
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +377 -9
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +52 -2
- package/dist/next.d.ts +52 -2
- package/dist/next.js +92 -1
- package/dist/next.js.map +1 -1
- package/dist/react.cjs +209 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +3 -2
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +406 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +111 -1
- package/dist/server.d.ts +111 -1
- package/dist/server.js +47 -1
- package/package.json +10 -9
- package/dist/chunk-7NAKOJGZ.js.map +0 -1
- package/dist/chunk-NNMUSWPG.js.map +0 -1
- /package/dist/{chunk-FUQ46RJV.js.map → chunk-GF6GUDZL.js.map} +0 -0
package/dist/react.js
CHANGED
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
pay,
|
|
10
10
|
pickWallet,
|
|
11
11
|
tip
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GF6GUDZL.js";
|
|
13
13
|
import {
|
|
14
14
|
clearPairing,
|
|
15
15
|
defineBwalletxSignin,
|
|
16
16
|
signInWithBwalletX
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-CVLXFNAG.js";
|
|
18
|
+
import "./chunk-MWD4BCO5.js";
|
|
18
19
|
|
|
19
20
|
// src/react.ts
|
|
20
21
|
import {
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React wrapper for <bwalletx-signin>. React is an optional peer dependency; the button itself is\n * the web component, so markup and behaviour are identical everywhere.\n *\n * import { BwalletxSignin } from '@bwalletx/connect/react';\n * <BwalletxSignin challengeUrl=\"/auth/bwalletx/challenge\" verifyUrl=\"/auth/bwalletx/verify\"\n * onSignedIn={(d) => router.refresh()} />\n */\nimport {\n createContext,\n createElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { WalletInterface } from '@bsv/sdk';\nimport { clearPairing } from './pair.js';\nimport { signInWithBwalletX, type Challenge } from './signin.js';\nimport { pairingModal } from './modal.js';\nimport { pickWallet } from './modal.js';\nimport { discoverWallets, type ConnectedWallet, type WalletOption } from './wallets.js';\nimport { PaymentError, buyToken, pay, tip, type BuyTokenRequest, type PayOptions, type PayRequest, type PayResult } from './pay.js';\nimport { defineBwalletxSignin, type SigninBrand, type SigninLabel, type SigninSize, type SigninTheme } from './element.js';\n\nexport interface SignedInDetail {\n identityKey: string;\n method: string;\n nonce: string;\n signature: number[];\n response: unknown;\n}\n\nexport interface BwalletxSigninProps {\n size?: SigninSize;\n theme?: SigninTheme;\n label?: SigninLabel;\n brand?: SigninBrand;\n /** true/undefined = default, false = hidden, string = custom text. */\n subtitle?: boolean | string;\n fullWidth?: boolean;\n loading?: boolean;\n disabled?: boolean;\n challengeUrl?: string;\n verifyUrl?: string;\n getUrl?: string;\n webUrl?: string;\n href?: string;\n authorizeUrl?: string;\n clientId?: string;\n redirectUri?: string;\n scope?: string;\n discoverMs?: number;\n 'aria-label'?: string;\n className?: string;\n style?: CSSProperties;\n id?: string;\n /** Every click. Call e.preventDefault() to skip the built-in flow. */\n onClick?: (e: CustomEvent<{ element: HTMLElement }>) => void;\n onSignedIn?: (d: SignedInDetail) => void;\n onNotFound?: () => void;\n onError?: (error: unknown) => void;\n}\n\nexport function BwalletxSignin(props: BwalletxSigninProps): ReactElement {\n const ref = useRef<HTMLElement>(null);\n const cb = useRef(props);\n cb.current = props;\n\n useEffect(() => {\n defineBwalletxSignin();\n const el = ref.current;\n if (!el) return;\n const on: [string, EventListener][] = [\n ['bwalletx-signin', (e) => cb.current.onClick?.(e as CustomEvent<{ element: HTMLElement }>)],\n ['bwalletx-signed-in', (e) => cb.current.onSignedIn?.((e as CustomEvent<SignedInDetail>).detail)],\n ['bwalletx-not-found', () => cb.current.onNotFound?.()],\n ['bwalletx-error', (e) => cb.current.onError?.((e as CustomEvent<{ error: unknown }>).detail.error)],\n ];\n for (const [t, f] of on) el.addEventListener(t, f);\n return () => { for (const [t, f] of on) el.removeEventListener(t, f); };\n }, []);\n\n const p = props;\n const flag = (v: boolean | undefined) => (v ? '' : undefined);\n return createElement('bwalletx-signin', {\n ref,\n id: p.id,\n class: p.className,\n style: p.style,\n size: p.size,\n theme: p.theme,\n label: p.label,\n brand: p.brand,\n subtitle: p.subtitle === false ? 'off' : typeof p.subtitle === 'string' ? p.subtitle : undefined,\n 'full-width': flag(p.fullWidth),\n loading: flag(p.loading),\n disabled: flag(p.disabled),\n 'challenge-url': p.challengeUrl,\n 'verify-url': p.verifyUrl,\n 'get-url': p.getUrl,\n 'web-url': p.webUrl,\n href: p.href,\n 'authorize-url': p.authorizeUrl,\n 'client-id': p.clientId,\n 'redirect-uri': p.redirectUri,\n scope: p.scope,\n 'discover-ms': p.discoverMs,\n 'aria-label': p['aria-label'],\n });\n}\n\nexport default BwalletxSignin;\n\nexport type BwalletXStatus = 'idle' | 'restoring' | 'connecting' | 'connected' | 'error';\n\nexport interface BwalletXState {\n status: BwalletXStatus;\n identityKey: string | null;\n handle: string | null;\n paymail: string | null;\n /** The BRC-100 wallet. null until connect() (a session restored from the cookie has no wallet yet). */\n wallet: WalletInterface | null;\n /** Which option connected (e.g. 'com.bwalletx.extension', 'bwalletx-pairing', 'handcash'). */\n walletId: string | null;\n /** True while the transitional HandCash adapter is in use. */\n transitional: boolean;\n /** The server session (verifyUrl/sessionUrl) is in place. */\n signedIn: boolean;\n error: Error | null;\n}\n\nexport interface BwalletXContextValue extends BwalletXState {\n /** Find wallets, show the picker if there is a choice, connect, and sign in if verifyUrl is set. */\n connect(opts?: { walletId?: string }): Promise<ConnectedWallet | null>;\n disconnect(): Promise<void>;\n /** The wallets found on the last connect() (or listWallets()). */\n wallets: WalletOption[];\n listWallets(): Promise<WalletOption[]>;\n pay(req: PayRequest): Promise<PayResult>;\n tip(req: PayRequest): Promise<PayResult>;\n buyToken(req: BuyTokenRequest): Promise<PayResult>;\n}\n\nexport interface BwalletXProviderProps {\n children?: ReactNode;\n /** POST { identityKey } → challenge. With verifyUrl, connect() also signs in. */\n challengeUrl?: string;\n /** POST { identityKey, nonce, signature } → { identityKey, handle?, paymail? } and sets the cookie. */\n verifyUrl?: string;\n /** POST to clear the cookie on disconnect(). */\n logoutUrl?: string;\n /** GET → { identityKey, handle?, paymail? } to restore a session on load. */\n sessionUrl?: string;\n /** Offer phone pairing with the built-in QR modal. Default true. */\n pairing?: boolean;\n /** Show the built-in picker when more than one wallet is available. Default true. */\n picker?: boolean;\n /** Extra options, e.g. [handcashAdapter({ login })]. */\n extraWallets?: WalletOption[];\n /** Probe desktop wallets on localhost. Default false. */\n probeLocal?: boolean;\n discoverMs?: number;\n payOptions?: PayOptions;\n onError?: (e: Error) => void;\n}\n\nconst empty: BwalletXState = {\n status: 'idle', identityKey: null, handle: null, paymail: null, wallet: null, walletId: null, transitional: false, signedIn: false, error: null,\n};\n\nconst Ctx = createContext<BwalletXContextValue | null>(null);\n\nasync function postJson(url: string, body: unknown): Promise<Record<string, unknown>> {\n const r = await fetch(url, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });\n const j = (await r.json().catch(() => ({}))) as Record<string, unknown>;\n if (!r.ok) throw Object.assign(new Error(String(j.error ?? `${url} answered ${r.status}`)), { status: r.status, code: j.code });\n return j;\n}\n\n/** Holds the connected wallet and session for the tree. Renders nothing itself; SSR-safe. */\nexport function BwalletXProvider(p: BwalletXProviderProps): ReactElement {\n const [state, setState] = useState<BwalletXState>(empty);\n const [wallets, setWallets] = useState<WalletOption[]>([]);\n const props = useRef(p);\n props.current = p;\n const payer = useRef<ConnectedWallet['payer']>(null);\n\n const fail = useCallback((e: unknown) => {\n const err = e instanceof Error ? e : new Error(String(e));\n setState((s) => ({ ...s, status: 'error', error: err }));\n props.current.onError?.(err);\n return err;\n }, []);\n\n useEffect(() => {\n const url = p.sessionUrl;\n if (!url) return;\n let live = true;\n setState((s) => ({ ...s, status: 'restoring' }));\n fetch(url, { credentials: 'same-origin' })\n .then((r) => r.json())\n .then((j: { identityKey?: string | null; handle?: string; paymail?: string }) => {\n if (!live) return;\n setState((s) =>\n j.identityKey\n ? { ...s, status: 'connected', identityKey: j.identityKey, handle: j.handle ?? null, paymail: j.paymail ?? null, signedIn: true }\n : { ...s, status: s.status === 'restoring' ? 'idle' : s.status },\n );\n })\n .catch(() => live && setState((s) => ({ ...s, status: s.status === 'restoring' ? 'idle' : s.status })));\n return () => {\n live = false;\n };\n }, [p.sessionUrl]);\n\n const listWallets = useCallback(async () => {\n const c = props.current;\n const list = await discoverWallets({\n ms: c.discoverMs,\n probeLocal: c.probeLocal,\n extra: c.extraWallets,\n pairing: c.pairing === false ? undefined : () => pairingModal(),\n });\n setWallets(list);\n return list;\n }, []);\n\n const connect = useCallback(\n async (opts: { walletId?: string } = {}) => {\n const c = props.current;\n setState((s) => ({ ...s, status: 'connecting', error: null }));\n try {\n const list = await listWallets();\n let choice: WalletOption | null | undefined = opts.walletId ? list.find((w) => w.id === opts.walletId) : undefined;\n if (!choice) {\n const direct = list.filter((w) => w.kind !== 'pairing');\n choice = c.picker !== false && list.length > 1 ? await pickWallet(list) : (direct[0] ?? list[0] ?? null);\n }\n if (!choice) {\n if (!list.length) throw Object.assign(new Error('No wallet found. Get bWalletX at bwalletx.com.'), { code: 'NOT_FOUND' });\n setState((s) => ({ ...s, status: s.identityKey ? 'connected' : 'idle' }));\n return null;\n }\n const cw = await choice.connect();\n let { handle = null, paymail = null } = cw as { handle?: string | null; paymail?: string | null };\n let signedIn = false;\n if (cw.wallet && c.challengeUrl && c.verifyUrl) {\n const r = await signInWithBwalletX({\n wallet: cw.wallet,\n challenge: (identityKey) => postJson(c.challengeUrl!, { identityKey }) as unknown as Promise<Challenge>,\n });\n const v = await postJson(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature });\n handle = (v.handle as string | undefined) ?? handle;\n paymail = (v.paymail as string | undefined) ?? paymail;\n signedIn = true;\n }\n payer.current = cw.payer;\n setState({\n status: 'connected',\n identityKey: cw.identityKey,\n handle,\n paymail,\n wallet: cw.wallet,\n walletId: cw.optionId,\n transitional: !!cw.transitional,\n signedIn,\n error: null,\n });\n return cw;\n } catch (e) {\n if ((e as Error)?.message === 'cancelled') {\n setState((s) => ({ ...s, status: s.identityKey ? 'connected' : 'idle' }));\n return null;\n }\n throw fail(e);\n }\n },\n [fail, listWallets],\n );\n\n const disconnect = useCallback(async () => {\n const c = props.current;\n if (c.logoutUrl) await postJson(c.logoutUrl, {}).catch(() => {});\n if (state.walletId === 'bwalletx-pairing') await clearPairing();\n payer.current = null;\n setState(empty);\n }, [state.walletId]);\n\n const need = () => {\n if (!payer.current) throw new PaymentError('NOT_SUPPORTED', 'Connect a wallet first (connect()).');\n return payer.current;\n };\n\n const value = useMemo<BwalletXContextValue>(\n () => ({\n ...state,\n wallets,\n connect,\n disconnect,\n listWallets,\n pay: (req) => pay(need(), req, props.current.payOptions),\n tip: (req) => tip(need(), req, props.current.payOptions),\n buyToken: (req) => buyToken(need(), req),\n }),\n [state, wallets, connect, disconnect, listWallets],\n );\n\n return createElement(Ctx.Provider, { value }, p.children);\n}\n\n/** The wallet + session state. Must be inside <BwalletXProvider>. */\nexport function useBwalletX(): BwalletXContextValue {\n const v = useContext(Ctx);\n if (!v) throw new Error('useBwalletX() must be used inside <BwalletXProvider>');\n return v;\n}\n\nexport { handcashAdapter, brc100Option, pairingOption, discoverWallets } from './wallets.js';\nexport type { WalletOption, ConnectedWallet, HandCashAdapterOptions } from './wallets.js';\nexport { PaymentError } from './pay.js';\nexport type { PayRequest, PayResult, BuyTokenRequest, PayOptions } from './pay.js';\n/** Alias with the casing used by <BwalletxSignin>. */\nexport const BwalletxProvider = BwalletXProvider;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/react.ts"],"sourcesContent":["/**\n * React wrapper for <bwalletx-signin>. React is an optional peer dependency; the button itself is\n * the web component, so markup and behaviour are identical everywhere.\n *\n * import { BwalletxSignin } from '@bwalletx/connect/react';\n * <BwalletxSignin challengeUrl=\"/auth/bwalletx/challenge\" verifyUrl=\"/auth/bwalletx/verify\"\n * onSignedIn={(d) => router.refresh()} />\n */\nimport {\n createContext,\n createElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport type { WalletInterface } from '@bsv/sdk';\nimport { clearPairing } from './pair.js';\nimport { signInWithBwalletX, type Challenge } from './signin.js';\nimport { pairingModal } from './modal.js';\nimport { pickWallet } from './modal.js';\nimport { discoverWallets, type ConnectedWallet, type WalletOption } from './wallets.js';\nimport { PaymentError, buyToken, pay, tip, type BuyTokenRequest, type PayOptions, type PayRequest, type PayResult } from './pay.js';\nimport { defineBwalletxSignin, type SigninBrand, type SigninLabel, type SigninSize, type SigninTheme } from './element.js';\n\nexport interface SignedInDetail {\n identityKey: string;\n method: string;\n nonce: string;\n signature: number[];\n response: unknown;\n}\n\nexport interface BwalletxSigninProps {\n size?: SigninSize;\n theme?: SigninTheme;\n label?: SigninLabel;\n brand?: SigninBrand;\n /** true/undefined = default, false = hidden, string = custom text. */\n subtitle?: boolean | string;\n fullWidth?: boolean;\n loading?: boolean;\n disabled?: boolean;\n challengeUrl?: string;\n verifyUrl?: string;\n getUrl?: string;\n webUrl?: string;\n href?: string;\n authorizeUrl?: string;\n clientId?: string;\n redirectUri?: string;\n scope?: string;\n discoverMs?: number;\n 'aria-label'?: string;\n className?: string;\n style?: CSSProperties;\n id?: string;\n /** Every click. Call e.preventDefault() to skip the built-in flow. */\n onClick?: (e: CustomEvent<{ element: HTMLElement }>) => void;\n onSignedIn?: (d: SignedInDetail) => void;\n onNotFound?: () => void;\n onError?: (error: unknown) => void;\n}\n\nexport function BwalletxSignin(props: BwalletxSigninProps): ReactElement {\n const ref = useRef<HTMLElement>(null);\n const cb = useRef(props);\n cb.current = props;\n\n useEffect(() => {\n defineBwalletxSignin();\n const el = ref.current;\n if (!el) return;\n const on: [string, EventListener][] = [\n ['bwalletx-signin', (e) => cb.current.onClick?.(e as CustomEvent<{ element: HTMLElement }>)],\n ['bwalletx-signed-in', (e) => cb.current.onSignedIn?.((e as CustomEvent<SignedInDetail>).detail)],\n ['bwalletx-not-found', () => cb.current.onNotFound?.()],\n ['bwalletx-error', (e) => cb.current.onError?.((e as CustomEvent<{ error: unknown }>).detail.error)],\n ];\n for (const [t, f] of on) el.addEventListener(t, f);\n return () => { for (const [t, f] of on) el.removeEventListener(t, f); };\n }, []);\n\n const p = props;\n const flag = (v: boolean | undefined) => (v ? '' : undefined);\n return createElement('bwalletx-signin', {\n ref,\n id: p.id,\n class: p.className,\n style: p.style,\n size: p.size,\n theme: p.theme,\n label: p.label,\n brand: p.brand,\n subtitle: p.subtitle === false ? 'off' : typeof p.subtitle === 'string' ? p.subtitle : undefined,\n 'full-width': flag(p.fullWidth),\n loading: flag(p.loading),\n disabled: flag(p.disabled),\n 'challenge-url': p.challengeUrl,\n 'verify-url': p.verifyUrl,\n 'get-url': p.getUrl,\n 'web-url': p.webUrl,\n href: p.href,\n 'authorize-url': p.authorizeUrl,\n 'client-id': p.clientId,\n 'redirect-uri': p.redirectUri,\n scope: p.scope,\n 'discover-ms': p.discoverMs,\n 'aria-label': p['aria-label'],\n });\n}\n\nexport default BwalletxSignin;\n\nexport type BwalletXStatus = 'idle' | 'restoring' | 'connecting' | 'connected' | 'error';\n\nexport interface BwalletXState {\n status: BwalletXStatus;\n identityKey: string | null;\n handle: string | null;\n paymail: string | null;\n /** The BRC-100 wallet. null until connect() (a session restored from the cookie has no wallet yet). */\n wallet: WalletInterface | null;\n /** Which option connected (e.g. 'com.bwalletx.extension', 'bwalletx-pairing', 'handcash'). */\n walletId: string | null;\n /** True while the transitional HandCash adapter is in use. */\n transitional: boolean;\n /** The server session (verifyUrl/sessionUrl) is in place. */\n signedIn: boolean;\n error: Error | null;\n}\n\nexport interface BwalletXContextValue extends BwalletXState {\n /** Find wallets, show the picker if there is a choice, connect, and sign in if verifyUrl is set. */\n connect(opts?: { walletId?: string }): Promise<ConnectedWallet | null>;\n disconnect(): Promise<void>;\n /** The wallets found on the last connect() (or listWallets()). */\n wallets: WalletOption[];\n listWallets(): Promise<WalletOption[]>;\n pay(req: PayRequest): Promise<PayResult>;\n tip(req: PayRequest): Promise<PayResult>;\n buyToken(req: BuyTokenRequest): Promise<PayResult>;\n}\n\nexport interface BwalletXProviderProps {\n children?: ReactNode;\n /** POST { identityKey } → challenge. With verifyUrl, connect() also signs in. */\n challengeUrl?: string;\n /** POST { identityKey, nonce, signature } → { identityKey, handle?, paymail? } and sets the cookie. */\n verifyUrl?: string;\n /** POST to clear the cookie on disconnect(). */\n logoutUrl?: string;\n /** GET → { identityKey, handle?, paymail? } to restore a session on load. */\n sessionUrl?: string;\n /** Offer phone pairing with the built-in QR modal. Default true. */\n pairing?: boolean;\n /** Show the built-in picker when more than one wallet is available. Default true. */\n picker?: boolean;\n /** Extra options, e.g. [handcashAdapter({ login })]. */\n extraWallets?: WalletOption[];\n /** Probe desktop wallets on localhost. Default false. */\n probeLocal?: boolean;\n discoverMs?: number;\n payOptions?: PayOptions;\n onError?: (e: Error) => void;\n}\n\nconst empty: BwalletXState = {\n status: 'idle', identityKey: null, handle: null, paymail: null, wallet: null, walletId: null, transitional: false, signedIn: false, error: null,\n};\n\nconst Ctx = createContext<BwalletXContextValue | null>(null);\n\nasync function postJson(url: string, body: unknown): Promise<Record<string, unknown>> {\n const r = await fetch(url, { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });\n const j = (await r.json().catch(() => ({}))) as Record<string, unknown>;\n if (!r.ok) throw Object.assign(new Error(String(j.error ?? `${url} answered ${r.status}`)), { status: r.status, code: j.code });\n return j;\n}\n\n/** Holds the connected wallet and session for the tree. Renders nothing itself; SSR-safe. */\nexport function BwalletXProvider(p: BwalletXProviderProps): ReactElement {\n const [state, setState] = useState<BwalletXState>(empty);\n const [wallets, setWallets] = useState<WalletOption[]>([]);\n const props = useRef(p);\n props.current = p;\n const payer = useRef<ConnectedWallet['payer']>(null);\n\n const fail = useCallback((e: unknown) => {\n const err = e instanceof Error ? e : new Error(String(e));\n setState((s) => ({ ...s, status: 'error', error: err }));\n props.current.onError?.(err);\n return err;\n }, []);\n\n useEffect(() => {\n const url = p.sessionUrl;\n if (!url) return;\n let live = true;\n setState((s) => ({ ...s, status: 'restoring' }));\n fetch(url, { credentials: 'same-origin' })\n .then((r) => r.json())\n .then((j: { identityKey?: string | null; handle?: string; paymail?: string }) => {\n if (!live) return;\n setState((s) =>\n j.identityKey\n ? { ...s, status: 'connected', identityKey: j.identityKey, handle: j.handle ?? null, paymail: j.paymail ?? null, signedIn: true }\n : { ...s, status: s.status === 'restoring' ? 'idle' : s.status },\n );\n })\n .catch(() => live && setState((s) => ({ ...s, status: s.status === 'restoring' ? 'idle' : s.status })));\n return () => {\n live = false;\n };\n }, [p.sessionUrl]);\n\n const listWallets = useCallback(async () => {\n const c = props.current;\n const list = await discoverWallets({\n ms: c.discoverMs,\n probeLocal: c.probeLocal,\n extra: c.extraWallets,\n pairing: c.pairing === false ? undefined : () => pairingModal(),\n });\n setWallets(list);\n return list;\n }, []);\n\n const connect = useCallback(\n async (opts: { walletId?: string } = {}) => {\n const c = props.current;\n setState((s) => ({ ...s, status: 'connecting', error: null }));\n try {\n const list = await listWallets();\n let choice: WalletOption | null | undefined = opts.walletId ? list.find((w) => w.id === opts.walletId) : undefined;\n if (!choice) {\n const direct = list.filter((w) => w.kind !== 'pairing');\n choice = c.picker !== false && list.length > 1 ? await pickWallet(list) : (direct[0] ?? list[0] ?? null);\n }\n if (!choice) {\n if (!list.length) throw Object.assign(new Error('No wallet found. Get bWalletX at bwalletx.com.'), { code: 'NOT_FOUND' });\n setState((s) => ({ ...s, status: s.identityKey ? 'connected' : 'idle' }));\n return null;\n }\n const cw = await choice.connect();\n let { handle = null, paymail = null } = cw as { handle?: string | null; paymail?: string | null };\n let signedIn = false;\n if (cw.wallet && c.challengeUrl && c.verifyUrl) {\n const r = await signInWithBwalletX({\n wallet: cw.wallet,\n challenge: (identityKey) => postJson(c.challengeUrl!, { identityKey }) as unknown as Promise<Challenge>,\n });\n const v = await postJson(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature });\n handle = (v.handle as string | undefined) ?? handle;\n paymail = (v.paymail as string | undefined) ?? paymail;\n signedIn = true;\n }\n payer.current = cw.payer;\n setState({\n status: 'connected',\n identityKey: cw.identityKey,\n handle,\n paymail,\n wallet: cw.wallet,\n walletId: cw.optionId,\n transitional: !!cw.transitional,\n signedIn,\n error: null,\n });\n return cw;\n } catch (e) {\n if ((e as Error)?.message === 'cancelled') {\n setState((s) => ({ ...s, status: s.identityKey ? 'connected' : 'idle' }));\n return null;\n }\n throw fail(e);\n }\n },\n [fail, listWallets],\n );\n\n const disconnect = useCallback(async () => {\n const c = props.current;\n if (c.logoutUrl) await postJson(c.logoutUrl, {}).catch(() => {});\n if (state.walletId === 'bwalletx-pairing') await clearPairing();\n payer.current = null;\n setState(empty);\n }, [state.walletId]);\n\n const need = () => {\n if (!payer.current) throw new PaymentError('NOT_SUPPORTED', 'Connect a wallet first (connect()).');\n return payer.current;\n };\n\n const value = useMemo<BwalletXContextValue>(\n () => ({\n ...state,\n wallets,\n connect,\n disconnect,\n listWallets,\n pay: (req) => pay(need(), req, props.current.payOptions),\n tip: (req) => tip(need(), req, props.current.payOptions),\n buyToken: (req) => buyToken(need(), req),\n }),\n [state, wallets, connect, disconnect, listWallets],\n );\n\n return createElement(Ctx.Provider, { value }, p.children);\n}\n\n/** The wallet + session state. Must be inside <BwalletXProvider>. */\nexport function useBwalletX(): BwalletXContextValue {\n const v = useContext(Ctx);\n if (!v) throw new Error('useBwalletX() must be used inside <BwalletXProvider>');\n return v;\n}\n\nexport { handcashAdapter, brc100Option, pairingOption, discoverWallets } from './wallets.js';\nexport type { WalletOption, ConnectedWallet, HandCashAdapterOptions } from './wallets.js';\nexport { PaymentError } from './pay.js';\nexport type { PayRequest, PayResult, BuyTokenRequest, PayOptions } from './pay.js';\n/** Alias with the casing used by <BwalletxSignin>. */\nexport const BwalletxProvider = BwalletXProvider;\n"],"mappings":"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","names":[]}
|
package/dist/server.cjs
CHANGED
|
@@ -20,30 +20,52 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/server.ts
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
|
+
ACCESS_CHALLENGE_LIFETIME_S: () => ACCESS_CHALLENGE_LIFETIME_S,
|
|
24
|
+
ACCESS_ERROR_STATUS: () => ACCESS_ERROR_STATUS,
|
|
25
|
+
ACCESS_PROTOCOL: () => ACCESS_PROTOCOL,
|
|
26
|
+
ACCESS_VERSION: () => ACCESS_VERSION,
|
|
27
|
+
AccessError: () => AccessError,
|
|
23
28
|
CHALLENGE_LIFETIME_MS: () => CHALLENGE_LIFETIME_MS,
|
|
24
29
|
IdentityLinkConflictError: () => IdentityLinkConflictError,
|
|
25
30
|
LOGIN_PROTOCOL: () => LOGIN_PROTOCOL,
|
|
31
|
+
MAX_HOLDING_PROOFS: () => MAX_HOLDING_PROOFS,
|
|
26
32
|
MIN_SECRET_LENGTH: () => MIN_SECRET_LENGTH,
|
|
33
|
+
PROTOCOL_MEDIA: () => PROTOCOL_MEDIA,
|
|
27
34
|
PaymentVerificationError: () => PaymentVerificationError,
|
|
35
|
+
accessErrorMessage: () => accessErrorMessage,
|
|
36
|
+
atLeastRaw: () => atLeastRaw,
|
|
37
|
+
bitSignResolver: () => bitSignResolver,
|
|
38
|
+
canonicalJson: () => canonicalJson,
|
|
39
|
+
challengeBytes: () => challengeBytes,
|
|
40
|
+
checkTokenHolding: () => checkTokenHolding,
|
|
41
|
+
createAccessChallenge: () => createAccessChallenge,
|
|
28
42
|
createChallenge: () => createChallenge,
|
|
29
43
|
isIdentityKey: () => isIdentityKey,
|
|
44
|
+
kindFor: () => kindFor,
|
|
30
45
|
linkIdentity: () => linkIdentity,
|
|
31
46
|
loginMessage: () => loginMessage,
|
|
32
47
|
memoryStore: () => memoryStore,
|
|
48
|
+
normaliseRequirement: () => normaliseRequirement,
|
|
49
|
+
oneSatResolver: () => oneSatResolver,
|
|
33
50
|
originAllowed: () => originAllowed,
|
|
51
|
+
parseAccessChallenge: () => parseAccessChallenge,
|
|
34
52
|
pgNonceTableSql: () => pgNonceTableSql,
|
|
35
53
|
pgStore: () => pgStore,
|
|
54
|
+
protocolMediaUrl: () => protocolMediaUrl,
|
|
36
55
|
readCookie: () => readCookie,
|
|
37
56
|
redisStore: () => redisStore,
|
|
38
57
|
serializeCookie: () => serializeCookie,
|
|
39
58
|
signInOrLink: () => signInOrLink,
|
|
40
59
|
signSession: () => signSession,
|
|
60
|
+
tokenNeedRaw: () => tokenNeedRaw,
|
|
61
|
+
verifyAccess: () => verifyAccess,
|
|
62
|
+
verifyHoldingProof: () => verifyHoldingProof,
|
|
41
63
|
verifyPayment: () => verifyPayment,
|
|
42
64
|
verifySession: () => verifySession,
|
|
43
65
|
verifySignIn: () => verifySignIn
|
|
44
66
|
});
|
|
45
67
|
module.exports = __toCommonJS(server_exports);
|
|
46
|
-
var
|
|
68
|
+
var import_sdk3 = require("@bsv/sdk");
|
|
47
69
|
|
|
48
70
|
// src/nonce-stores.ts
|
|
49
71
|
var ttlSeconds = (c) => Math.max(1, Math.ceil((c.expiresAt - Date.now()) / 1e3));
|
|
@@ -349,6 +371,359 @@ async function signInOrLink(identityKey, o) {
|
|
|
349
371
|
return { status: "created", userId, identityKey: key };
|
|
350
372
|
}
|
|
351
373
|
|
|
374
|
+
// src/access.ts
|
|
375
|
+
var ACCESS_VERSION = 1;
|
|
376
|
+
var ACCESS_PROTOCOL = [2, "bwalletx access"];
|
|
377
|
+
var ACCESS_CHALLENGE_LIFETIME_S = 120;
|
|
378
|
+
var MAX_HOLDING_PROOFS = 50;
|
|
379
|
+
var ACCESS_ERROR_STATUS = {
|
|
380
|
+
not_installed: 424,
|
|
381
|
+
user_rejected: 403,
|
|
382
|
+
wrong_account: 409,
|
|
383
|
+
insufficient_tokens: 403,
|
|
384
|
+
identity_level_too_low: 403,
|
|
385
|
+
kyc_required: 403,
|
|
386
|
+
indexer_unavailable: 503,
|
|
387
|
+
expired: 401,
|
|
388
|
+
replay: 401,
|
|
389
|
+
invalid_signature: 401,
|
|
390
|
+
bad_request: 400
|
|
391
|
+
};
|
|
392
|
+
var AccessError = class extends Error {
|
|
393
|
+
code;
|
|
394
|
+
have;
|
|
395
|
+
need;
|
|
396
|
+
constructor(code, message, extra = {}) {
|
|
397
|
+
super(message ?? code);
|
|
398
|
+
this.name = "AccessError";
|
|
399
|
+
this.code = code;
|
|
400
|
+
this.have = extra.have;
|
|
401
|
+
this.need = extra.need;
|
|
402
|
+
}
|
|
403
|
+
toJSON() {
|
|
404
|
+
const b = { error: this.code, message: this.message };
|
|
405
|
+
if (this.have !== void 0) b.have = this.have;
|
|
406
|
+
if (this.need !== void 0) b.need = this.need;
|
|
407
|
+
return b;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var PROTOCOL_MEDIA = {
|
|
411
|
+
"401": { name: "Identity", color: "#22c55e", video: "assets/protocol/401.mp4", poster: "assets/protocol/401.jpg" },
|
|
412
|
+
"402": { name: "Payment", color: "#3b82f6", video: "assets/protocol/402.mp4", poster: "assets/protocol/402.jpg" },
|
|
413
|
+
"403": { name: "Access", color: "#ef4444", video: "assets/protocol/403.mp4", poster: "assets/protocol/403.jpg" }
|
|
414
|
+
};
|
|
415
|
+
function protocolMediaUrl(kind, which, version = "latest") {
|
|
416
|
+
return `https://unpkg.com/@bwalletx/connect@${version}/${PROTOCOL_MEDIA[kind][which]}`;
|
|
417
|
+
}
|
|
418
|
+
function canonicalJson(v) {
|
|
419
|
+
if (v === null) return "null";
|
|
420
|
+
if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
|
|
421
|
+
if (typeof v === "number") {
|
|
422
|
+
if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
|
|
423
|
+
return JSON.stringify(v);
|
|
424
|
+
}
|
|
425
|
+
if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
|
|
426
|
+
if (typeof v === "object") {
|
|
427
|
+
const o = v;
|
|
428
|
+
const keys2 = Object.keys(o).filter((k) => o[k] !== void 0).sort();
|
|
429
|
+
return `{${keys2.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
|
|
430
|
+
}
|
|
431
|
+
throw new Error(`canonicalJson: unsupported ${typeof v}`);
|
|
432
|
+
}
|
|
433
|
+
function challengeBytes(challenge) {
|
|
434
|
+
return Array.from(new TextEncoder().encode(challenge));
|
|
435
|
+
}
|
|
436
|
+
var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
|
|
437
|
+
var RAW = /^[1-9]\d{0,38}$/;
|
|
438
|
+
function normaliseRequirement(r) {
|
|
439
|
+
if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
|
|
440
|
+
const req = r;
|
|
441
|
+
const out = {};
|
|
442
|
+
if (req.token !== void 0) {
|
|
443
|
+
const t = req.token;
|
|
444
|
+
if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
|
|
445
|
+
const id = t.id.trim().toLowerCase();
|
|
446
|
+
if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
|
|
447
|
+
const hasMin = t.min !== void 0;
|
|
448
|
+
const hasBps = t.minBps !== void 0;
|
|
449
|
+
if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
|
|
450
|
+
out.token = { id };
|
|
451
|
+
if (hasMin) {
|
|
452
|
+
if (typeof t.min !== "string" || !RAW.test(t.min)) throw new AccessError("bad_request", "require.token.min must be a positive integer string (raw units)");
|
|
453
|
+
out.token.min = t.min;
|
|
454
|
+
} else {
|
|
455
|
+
if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
|
|
456
|
+
throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
|
|
457
|
+
}
|
|
458
|
+
out.token.minBps = t.minBps;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (req.identityLevel !== void 0) {
|
|
462
|
+
if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
|
|
463
|
+
throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
|
|
464
|
+
}
|
|
465
|
+
out.identityLevel = req.identityLevel;
|
|
466
|
+
}
|
|
467
|
+
if (req.kyc !== void 0) {
|
|
468
|
+
if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
|
|
469
|
+
if (req.kyc) out.kyc = true;
|
|
470
|
+
}
|
|
471
|
+
if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
function kindFor(r) {
|
|
475
|
+
return r.token ? "403" : "401";
|
|
476
|
+
}
|
|
477
|
+
function parseAccessChallenge(s) {
|
|
478
|
+
if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
|
|
479
|
+
let c;
|
|
480
|
+
try {
|
|
481
|
+
c = JSON.parse(s);
|
|
482
|
+
} catch {
|
|
483
|
+
throw new AccessError("bad_request", "challenge is not JSON");
|
|
484
|
+
}
|
|
485
|
+
if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
|
|
486
|
+
if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
|
|
487
|
+
if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
|
|
488
|
+
throw new AccessError("bad_request", "bad origin or nonce");
|
|
489
|
+
}
|
|
490
|
+
if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
|
|
491
|
+
if (c.sub !== void 0 && (typeof c.sub !== "string" || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError("bad_request", "bad sub");
|
|
492
|
+
c.require = normaliseRequirement(c.require);
|
|
493
|
+
if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
|
|
494
|
+
return c;
|
|
495
|
+
}
|
|
496
|
+
function tokenNeedRaw(t, supplyRaw) {
|
|
497
|
+
if (t.min) return t.min;
|
|
498
|
+
if (supplyRaw === void 0 || !/^\d+$/.test(supplyRaw)) throw new AccessError("indexer_unavailable", "token supply unknown");
|
|
499
|
+
const n = BigInt(supplyRaw) * BigInt(t.minBps);
|
|
500
|
+
const need = (n + 9999n) / 10000n;
|
|
501
|
+
return (need < 1n ? 1n : need).toString();
|
|
502
|
+
}
|
|
503
|
+
function atLeastRaw(have, need) {
|
|
504
|
+
try {
|
|
505
|
+
return BigInt(have) >= BigInt(need);
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function accessErrorMessage(code, e = {}) {
|
|
511
|
+
switch (code) {
|
|
512
|
+
case "not_installed":
|
|
513
|
+
return "No bWalletX (or other BRC-100 wallet) was found.";
|
|
514
|
+
case "user_rejected":
|
|
515
|
+
return "The request was declined in the wallet.";
|
|
516
|
+
case "wrong_account":
|
|
517
|
+
return "The wallet is signed in to a different account than this site expects.";
|
|
518
|
+
case "insufficient_tokens":
|
|
519
|
+
return `Not enough tokens: holding ${e.have ?? 0}, need ${e.need ?? "?"}.`;
|
|
520
|
+
case "identity_level_too_low":
|
|
521
|
+
return `Identity level ${e.have ?? 0} is below the required level ${e.need ?? "?"}.`;
|
|
522
|
+
case "kyc_required":
|
|
523
|
+
return "A verified identity (KYC) is required.";
|
|
524
|
+
case "indexer_unavailable":
|
|
525
|
+
return "Holdings could not be checked right now. Try again shortly.";
|
|
526
|
+
case "expired":
|
|
527
|
+
return "The request expired. Try again.";
|
|
528
|
+
case "replay":
|
|
529
|
+
return "That request was already used. Try again.";
|
|
530
|
+
case "invalid_signature":
|
|
531
|
+
return "The wallet signature did not verify.";
|
|
532
|
+
case "bad_request":
|
|
533
|
+
return "The request was malformed.";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/access-server.ts
|
|
538
|
+
var import_sdk2 = require("@bsv/sdk");
|
|
539
|
+
var bare = (tokenId) => tokenId.replace(/^bsv21:/, "");
|
|
540
|
+
function oneSatResolver(o = {}) {
|
|
541
|
+
const base = (o.base ?? "https://api.1sat.app/1sat").replace(/\/$/, "");
|
|
542
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
543
|
+
const t = o.timeoutMs ?? 12e3;
|
|
544
|
+
const call = async (path, init) => {
|
|
545
|
+
const res = await f(`${base}${path}`, { ...init, signal: AbortSignal.timeout(t), cache: "no-store" });
|
|
546
|
+
if (!res.ok) throw new Error(`1sat ${res.status} for ${path}`);
|
|
547
|
+
return res.json();
|
|
548
|
+
};
|
|
549
|
+
return {
|
|
550
|
+
async tokenBalance(tokenId, addresses) {
|
|
551
|
+
const sorted = [...new Set(addresses)].sort();
|
|
552
|
+
let total = 0n;
|
|
553
|
+
for (let i = 0; i < sorted.length; i += 100) {
|
|
554
|
+
const j = await call(`/bsv21/${bare(tokenId)}/p2pkh/balance`, {
|
|
555
|
+
method: "POST",
|
|
556
|
+
headers: { "content-type": "application/json" },
|
|
557
|
+
body: JSON.stringify(sorted.slice(i, i + 100))
|
|
558
|
+
});
|
|
559
|
+
const b = j?.balance ?? 0;
|
|
560
|
+
const s = typeof b === "number" ? String(Math.trunc(b)) : String(b).split(".")[0] || "0";
|
|
561
|
+
if (!/^\d+$/.test(s)) throw new Error("1sat returned a bad balance");
|
|
562
|
+
total += BigInt(s);
|
|
563
|
+
}
|
|
564
|
+
return total.toString();
|
|
565
|
+
},
|
|
566
|
+
async tokenSupply(tokenId) {
|
|
567
|
+
const j = await call(`/bsv21/${bare(tokenId)}`);
|
|
568
|
+
const tok = (j?.token ?? j) || {};
|
|
569
|
+
const v = tok.amt ?? tok.max ?? tok.supply;
|
|
570
|
+
const s = v === void 0 || v === null ? "" : String(v).split(".")[0];
|
|
571
|
+
if (!/^\d+$/.test(s) || s === "0") throw new Error("1sat did not report a supply");
|
|
572
|
+
return s;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function bitSignResolver(o = {}) {
|
|
577
|
+
const base = (o.base ?? "https://bit-sign.online").replace(/\/$/, "");
|
|
578
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
579
|
+
const tokens = o.tokens ?? oneSatResolver({ fetch: f });
|
|
580
|
+
return {
|
|
581
|
+
tokenBalance: (id, a) => tokens.tokenBalance(id, a),
|
|
582
|
+
tokenSupply: tokens.tokenSupply ? (id) => tokens.tokenSupply(id) : void 0,
|
|
583
|
+
async identity(identityKey, response) {
|
|
584
|
+
const res = await f(`${base}/api/access/verify`, {
|
|
585
|
+
method: "POST",
|
|
586
|
+
headers: { "content-type": "application/json" },
|
|
587
|
+
body: JSON.stringify({ response, only: "identity" }),
|
|
588
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 12e3),
|
|
589
|
+
cache: "no-store"
|
|
590
|
+
});
|
|
591
|
+
const j = await res.json().catch(() => null);
|
|
592
|
+
if (!j?.identity || j.identityKey !== identityKey) throw new Error(`bit-sign ${res.status}`);
|
|
593
|
+
return { level: Number(j.identity.level) || 0, kyc: j.identity.kyc === true };
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function normalOrigin(origin) {
|
|
598
|
+
const u = new URL(origin);
|
|
599
|
+
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
600
|
+
return u.origin;
|
|
601
|
+
}
|
|
602
|
+
function randomNonce() {
|
|
603
|
+
const b = new Uint8Array(18);
|
|
604
|
+
globalThis.crypto.getRandomValues(b);
|
|
605
|
+
let s = "";
|
|
606
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
607
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
608
|
+
}
|
|
609
|
+
async function createAccessChallenge(o) {
|
|
610
|
+
const origin = normalOrigin(o.origin);
|
|
611
|
+
const require2 = normaliseRequirement(o.require);
|
|
612
|
+
let sub;
|
|
613
|
+
if (o.identityKey !== void 0 && o.identityKey !== null && o.identityKey !== "") {
|
|
614
|
+
if (!isIdentityKey(o.identityKey)) throw new AccessError("bad_request", "identityKey must be a compressed public key");
|
|
615
|
+
sub = o.identityKey.toLowerCase();
|
|
616
|
+
}
|
|
617
|
+
const iat = Math.floor((o.now ?? Date.now()) / 1e3);
|
|
618
|
+
const exp = iat + (o.lifetimeS ?? ACCESS_CHALLENGE_LIFETIME_S);
|
|
619
|
+
const nonce = randomNonce();
|
|
620
|
+
const c = { v: 1, kind: o.kind ?? kindFor(require2), origin, nonce, iat, exp, sub, require: require2 };
|
|
621
|
+
const challenge = canonicalJson(c);
|
|
622
|
+
await o.store.put(nonce, { identityKey: sub ?? "", origin, message: challenge, expiresAt: exp * 1e3 });
|
|
623
|
+
return { challenge, nonce, kind: c.kind, protocolID: ACCESS_PROTOCOL, keyID: nonce, exp };
|
|
624
|
+
}
|
|
625
|
+
var isByteArray = (s) => Array.isArray(s) && s.length >= 8 && s.length <= 80 && s.every((b) => Number.isInteger(b) && b >= 0 && b <= 255);
|
|
626
|
+
function verifyHoldingProof(challenge, p) {
|
|
627
|
+
const proof = p;
|
|
628
|
+
if (!proof || typeof proof.pubkey !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(proof.pubkey) || !isByteArray(proof.signature)) return null;
|
|
629
|
+
try {
|
|
630
|
+
const pub = import_sdk2.PublicKey.fromString(proof.pubkey);
|
|
631
|
+
const sig = import_sdk2.Signature.fromDER(proof.signature);
|
|
632
|
+
return pub.verify(challengeBytes(challenge), sig) ? pub.toAddress() : null;
|
|
633
|
+
} catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function verifyAccess(o) {
|
|
638
|
+
const r = o.response;
|
|
639
|
+
if (!r || typeof r !== "object" || r.v !== 1) throw new AccessError("bad_request", "response must be { v: 1, challenge, identityKey, signature }");
|
|
640
|
+
const c = parseAccessChallenge(r.challenge);
|
|
641
|
+
const nowS = Math.floor((o.now ?? Date.now()) / 1e3);
|
|
642
|
+
const pending = await o.store.take(c.nonce);
|
|
643
|
+
if (!pending) throw new AccessError(nowS >= c.exp ? "expired" : "replay");
|
|
644
|
+
if (nowS >= c.exp || (o.now ?? Date.now()) > pending.expiresAt) throw new AccessError("expired");
|
|
645
|
+
if (pending.message !== r.challenge) throw new AccessError("replay", "challenge does not match the one issued");
|
|
646
|
+
let origin;
|
|
647
|
+
try {
|
|
648
|
+
origin = normalOrigin(o.origin);
|
|
649
|
+
} catch {
|
|
650
|
+
throw new AccessError("bad_request", "server origin is misconfigured");
|
|
651
|
+
}
|
|
652
|
+
if (c.origin !== origin || pending.origin !== origin) throw new AccessError("bad_request", "origin mismatch");
|
|
653
|
+
if (o.require && canonicalJson(normaliseRequirement(o.require)) !== canonicalJson(c.require)) {
|
|
654
|
+
throw new AccessError("bad_request", "challenge requirement does not match this gate");
|
|
655
|
+
}
|
|
656
|
+
if (!isIdentityKey(r.identityKey)) throw new AccessError("bad_request", "identityKey must be a compressed public key");
|
|
657
|
+
const identityKey = r.identityKey.toLowerCase();
|
|
658
|
+
if (c.sub && c.sub !== identityKey) throw new AccessError("wrong_account");
|
|
659
|
+
if (!isByteArray(r.signature)) throw new AccessError("invalid_signature");
|
|
660
|
+
let valid = false;
|
|
661
|
+
try {
|
|
662
|
+
({ valid } = await new import_sdk2.ProtoWallet("anyone").verifySignature({
|
|
663
|
+
protocolID: ACCESS_PROTOCOL,
|
|
664
|
+
keyID: c.nonce,
|
|
665
|
+
counterparty: identityKey,
|
|
666
|
+
data: challengeBytes(r.challenge),
|
|
667
|
+
signature: r.signature
|
|
668
|
+
}));
|
|
669
|
+
} catch {
|
|
670
|
+
valid = false;
|
|
671
|
+
}
|
|
672
|
+
if (!valid) throw new AccessError("invalid_signature");
|
|
673
|
+
const grant = { ok: true, identityKey, kind: c.kind, require: c.require, addresses: [], have: {} };
|
|
674
|
+
if (c.require.identityLevel || c.require.kyc) {
|
|
675
|
+
if (!o.resolver.identity) throw new AccessError("indexer_unavailable", "no identity resolver configured");
|
|
676
|
+
let id;
|
|
677
|
+
try {
|
|
678
|
+
id = await o.resolver.identity(identityKey, r);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
throw new AccessError("indexer_unavailable", `identity lookup failed: ${e.message}`);
|
|
681
|
+
}
|
|
682
|
+
grant.have.identityLevel = id.level;
|
|
683
|
+
grant.have.kyc = id.kyc;
|
|
684
|
+
if (c.require.kyc && !id.kyc) throw new AccessError("kyc_required");
|
|
685
|
+
if (c.require.identityLevel && id.level < c.require.identityLevel) {
|
|
686
|
+
throw new AccessError("identity_level_too_low", void 0, { have: id.level, need: c.require.identityLevel });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (c.require.token) {
|
|
690
|
+
const proofs = Array.isArray(r.holdings) ? r.holdings.slice(0, MAX_HOLDING_PROOFS) : [];
|
|
691
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
692
|
+
for (const p of proofs) {
|
|
693
|
+
const a = verifyHoldingProof(r.challenge, p);
|
|
694
|
+
if (a) addresses.add(a);
|
|
695
|
+
}
|
|
696
|
+
grant.addresses = [...addresses].sort();
|
|
697
|
+
const check = await checkTokenHolding(c.require, grant.addresses, o.resolver);
|
|
698
|
+
grant.have.tokenRaw = check.have;
|
|
699
|
+
grant.have.needRaw = check.need;
|
|
700
|
+
}
|
|
701
|
+
return grant;
|
|
702
|
+
}
|
|
703
|
+
async function checkTokenHolding(require2, addresses, resolver) {
|
|
704
|
+
const t = require2.token;
|
|
705
|
+
if (!t) return { have: "0", need: "0" };
|
|
706
|
+
let supply;
|
|
707
|
+
if (t.minBps) {
|
|
708
|
+
if (!resolver.tokenSupply) throw new AccessError("indexer_unavailable", "no supply resolver configured");
|
|
709
|
+
try {
|
|
710
|
+
supply = await resolver.tokenSupply(t.id);
|
|
711
|
+
} catch (e) {
|
|
712
|
+
throw new AccessError("indexer_unavailable", `supply lookup failed: ${e.message}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const need = tokenNeedRaw(t, supply);
|
|
716
|
+
if (!addresses.length) throw new AccessError("insufficient_tokens", void 0, { have: "0", need });
|
|
717
|
+
let have;
|
|
718
|
+
try {
|
|
719
|
+
have = await resolver.tokenBalance(t.id, addresses);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
throw new AccessError("indexer_unavailable", `balance lookup failed: ${e.message}`);
|
|
722
|
+
}
|
|
723
|
+
if (!atLeastRaw(have, need)) throw new AccessError("insufficient_tokens", void 0, { have, need });
|
|
724
|
+
return { have, need };
|
|
725
|
+
}
|
|
726
|
+
|
|
352
727
|
// src/server.ts
|
|
353
728
|
var LOGIN_PROTOCOL = [2, "bwallet sign in"];
|
|
354
729
|
var CHALLENGE_LIFETIME_MS = 2 * 60 * 1e3;
|
|
@@ -380,13 +755,13 @@ var storeOrDefault = (s) => s ?? (defaultStore ??= memoryStore());
|
|
|
380
755
|
function isIdentityKey(x) {
|
|
381
756
|
if (typeof x !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;
|
|
382
757
|
try {
|
|
383
|
-
|
|
758
|
+
import_sdk3.PublicKey.fromString(x);
|
|
384
759
|
return true;
|
|
385
760
|
} catch {
|
|
386
761
|
return false;
|
|
387
762
|
}
|
|
388
763
|
}
|
|
389
|
-
function
|
|
764
|
+
function normalOrigin2(origin) {
|
|
390
765
|
const u = new URL(origin);
|
|
391
766
|
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) {
|
|
392
767
|
throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
@@ -404,7 +779,7 @@ function loginMessage(origin, nonce, expiresAt) {
|
|
|
404
779
|
`Expires: ${new Date(expiresAt).toISOString()}`
|
|
405
780
|
].join("\n");
|
|
406
781
|
}
|
|
407
|
-
function
|
|
782
|
+
function randomNonce2() {
|
|
408
783
|
const b = new Uint8Array(16);
|
|
409
784
|
globalThis.crypto.getRandomValues(b);
|
|
410
785
|
let s = "";
|
|
@@ -413,8 +788,8 @@ function randomNonce() {
|
|
|
413
788
|
}
|
|
414
789
|
async function createChallenge(opts) {
|
|
415
790
|
if (!isIdentityKey(opts.identityKey)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
|
|
416
|
-
const origin =
|
|
417
|
-
const nonce =
|
|
791
|
+
const origin = normalOrigin2(opts.origin);
|
|
792
|
+
const nonce = randomNonce2();
|
|
418
793
|
const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;
|
|
419
794
|
const message = loginMessage(origin, nonce, expiresAt);
|
|
420
795
|
await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });
|
|
@@ -426,7 +801,7 @@ async function verifySignIn(opts) {
|
|
|
426
801
|
if (!c || Date.now() > c.expiresAt) return null;
|
|
427
802
|
let origin;
|
|
428
803
|
try {
|
|
429
|
-
origin =
|
|
804
|
+
origin = normalOrigin2(opts.origin);
|
|
430
805
|
} catch {
|
|
431
806
|
return null;
|
|
432
807
|
}
|
|
@@ -439,7 +814,7 @@ async function verifySignIn(opts) {
|
|
|
439
814
|
return null;
|
|
440
815
|
}
|
|
441
816
|
try {
|
|
442
|
-
const { valid } = await new
|
|
817
|
+
const { valid } = await new import_sdk3.ProtoWallet("anyone").verifySignature({
|
|
443
818
|
protocolID: LOGIN_PROTOCOL,
|
|
444
819
|
keyID: opts.nonce,
|
|
445
820
|
counterparty: identityKey,
|
|
@@ -453,31 +828,53 @@ async function verifySignIn(opts) {
|
|
|
453
828
|
}
|
|
454
829
|
function originAllowed(requestOrigin, origin) {
|
|
455
830
|
try {
|
|
456
|
-
return !!requestOrigin && requestOrigin ===
|
|
831
|
+
return !!requestOrigin && requestOrigin === normalOrigin2(origin);
|
|
457
832
|
} catch {
|
|
458
833
|
return false;
|
|
459
834
|
}
|
|
460
835
|
}
|
|
461
836
|
// Annotate the CommonJS export names for ESM import in node:
|
|
462
837
|
0 && (module.exports = {
|
|
838
|
+
ACCESS_CHALLENGE_LIFETIME_S,
|
|
839
|
+
ACCESS_ERROR_STATUS,
|
|
840
|
+
ACCESS_PROTOCOL,
|
|
841
|
+
ACCESS_VERSION,
|
|
842
|
+
AccessError,
|
|
463
843
|
CHALLENGE_LIFETIME_MS,
|
|
464
844
|
IdentityLinkConflictError,
|
|
465
845
|
LOGIN_PROTOCOL,
|
|
846
|
+
MAX_HOLDING_PROOFS,
|
|
466
847
|
MIN_SECRET_LENGTH,
|
|
848
|
+
PROTOCOL_MEDIA,
|
|
467
849
|
PaymentVerificationError,
|
|
850
|
+
accessErrorMessage,
|
|
851
|
+
atLeastRaw,
|
|
852
|
+
bitSignResolver,
|
|
853
|
+
canonicalJson,
|
|
854
|
+
challengeBytes,
|
|
855
|
+
checkTokenHolding,
|
|
856
|
+
createAccessChallenge,
|
|
468
857
|
createChallenge,
|
|
469
858
|
isIdentityKey,
|
|
859
|
+
kindFor,
|
|
470
860
|
linkIdentity,
|
|
471
861
|
loginMessage,
|
|
472
862
|
memoryStore,
|
|
863
|
+
normaliseRequirement,
|
|
864
|
+
oneSatResolver,
|
|
473
865
|
originAllowed,
|
|
866
|
+
parseAccessChallenge,
|
|
474
867
|
pgNonceTableSql,
|
|
475
868
|
pgStore,
|
|
869
|
+
protocolMediaUrl,
|
|
476
870
|
readCookie,
|
|
477
871
|
redisStore,
|
|
478
872
|
serializeCookie,
|
|
479
873
|
signInOrLink,
|
|
480
874
|
signSession,
|
|
875
|
+
tokenNeedRaw,
|
|
876
|
+
verifyAccess,
|
|
877
|
+
verifyHoldingProof,
|
|
481
878
|
verifyPayment,
|
|
482
879
|
verifySession,
|
|
483
880
|
verifySignIn
|