@bwalletx/connect 0.3.0 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +145 -4
  3. package/assets/protocol/401.jpg +0 -0
  4. package/assets/protocol/401.mp4 +0 -0
  5. package/assets/protocol/402.jpg +0 -0
  6. package/assets/protocol/402.mp4 +0 -0
  7. package/assets/protocol/403.jpg +0 -0
  8. package/assets/protocol/403.mp4 +0 -0
  9. package/dist/access-Z6xQwKVF.d.cts +118 -0
  10. package/dist/access-Z6xQwKVF.d.ts +118 -0
  11. package/dist/bwalletx-connect.global.js +41 -3
  12. package/dist/bwalletx-connect.global.js.map +1 -1
  13. package/dist/{chunk-NNMUSWPG.js → chunk-CVLXFNAG.js} +123 -1
  14. package/dist/chunk-CVLXFNAG.js.map +1 -0
  15. package/dist/chunk-GF6GUDZL.js +404 -0
  16. package/dist/chunk-GF6GUDZL.js.map +1 -0
  17. package/dist/chunk-MWD4BCO5.js +182 -0
  18. package/dist/chunk-MWD4BCO5.js.map +1 -0
  19. package/dist/chunk-QRXFL66W.js +652 -0
  20. package/dist/chunk-QRXFL66W.js.map +1 -0
  21. package/dist/element.cjs +209 -0
  22. package/dist/element.cjs.map +1 -1
  23. package/dist/element.js +2 -1
  24. package/dist/element.js.map +1 -1
  25. package/dist/index.cjs +706 -5
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +66 -62
  28. package/dist/index.d.ts +66 -62
  29. package/dist/index.js +63 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/next.cjs +783 -0
  32. package/dist/next.cjs.map +1 -0
  33. package/dist/next.d.cts +98 -0
  34. package/dist/next.d.ts +98 -0
  35. package/dist/next.js +198 -0
  36. package/dist/next.js.map +1 -0
  37. package/dist/react.cjs +795 -36
  38. package/dist/react.cjs.map +1 -1
  39. package/dist/react.d.cts +74 -28
  40. package/dist/react.d.ts +74 -28
  41. package/dist/react.js +177 -4
  42. package/dist/react.js.map +1 -1
  43. package/dist/server.cjs +742 -10
  44. package/dist/server.cjs.map +1 -1
  45. package/dist/server.d.cts +314 -2
  46. package/dist/server.d.ts +314 -2
  47. package/dist/server.js +82 -107
  48. package/dist/server.js.map +1 -1
  49. package/dist/wallets-VOKIKBbO.d.cts +250 -0
  50. package/dist/wallets-VOKIKBbO.d.ts +250 -0
  51. package/package.json +32 -6
  52. package/dist/chunk-NNMUSWPG.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0 (unreleased)
4
+
5
+ - **Access protocol v1 ($401 identity / $403 token-gated access)**, spec in `docs/ACCESS-PROTOCOL.md`. Standard BRC-100 calls only, so any BRC-100 wallet works.
6
+ - Client: `requestAccess()` signs the site's challenge and proves the keys that hold the token (`proveHoldings()`). Errors are `AccessError` with a protocol code.
7
+ - Server: `createAccessChallenge`, `verifyAccess`, `checkTokenHolding`, plus `oneSatResolver()` (1Sat indexer) and `bitSignResolver()` (identity level / KYC). Fails closed: an indexer outage is `indexer_unavailable`, never a grant.
8
+ - Next.js: `createBwalletXAccess()` gives challenge, verify, status and logout routes, a signed access cookie, and `getAccess()`, which re-checks holdings every `recheckEvery` seconds so selling the tokens ends access.
9
+ - `<bwalletx-signin require-token="bsv21:…" min="100">` runs the access flow and emits `bwalletx-access-granted` / `bwalletx-access-denied`.
10
+ - Protocol videos and posters in `assets/protocol/` (401 green, 402 blue, 403 red), exported as `PROTOCOL_MEDIA`.
11
+
12
+ ## 0.4.1 (2026-10-12)
13
+
14
+ - React: `BwalletXProvider` and `BwalletxSignin` declare `ReactElement` return types, so they type-check as JSX under React 18 `@types/react`.
15
+ - First public release of the 0.4 line (0.4.0 was staged on npm but never released).
16
+
17
+ ## 0.4.0 (2026-10-12)
18
+
19
+ Everything from 0.3 still works the same way. All additions are BRC-100 first: bWalletX is the main target, but any BRC-100 wallet goes through the same interface.
20
+
21
+ - **React** (`@bwalletx/connect/react`): `<BwalletXProvider>` and `useBwalletX()` give you status, identityKey, handle and paymail, connect/disconnect, the BRC-100 `wallet`, and pay/tip/buyToken. Safe to server-render. `<BwalletxSignin>` is unchanged.
22
+ - **Next.js adapter** (`@bwalletx/connect/next`, new): `createBwalletXAuth()` gives you challenge, verify, logout and session route handlers, an HMAC-signed httpOnly session cookie, and `getSession()` for server components. The secret comes from `BWALLETX_SESSION_SECRET` (or `secret`). There is no default, so it throws if the secret is missing.
23
+ - **Nonce stores**: `redisStore` (Upstash, node-redis, ioredis) and `pgStore` (any `query` function), plus `pgNonceTableSql()`. `memoryStore` now warns when it runs in production.
24
+ - **Sessions**: `signSession`, `verifySession`, `serializeCookie` and `readCookie` (WebCrypto, so they work on Edge).
25
+ - **Payments**: `pay` (to a paymail, address or BRC-29 identity key; in sats or USD; optional OP_RETURN memo), `tip`, and a `buyToken` quote hook. Each makes one `createAction` and is never retried. Errors are `PaymentError` with a `code` and a `maybeSent` flag.
26
+ - **Server**: `verifyPayment` checks a txid, raw tx, BEEF or Atomic BEEF against the expected outputs, with optional WhatsOnChain or ARC status checks and SPV.
27
+ - **Account linking**: `linkIdentity`, `signInOrLink`, and `IdentityLinkConflictError` (the Next adapter turns it into a 409).
28
+ - **Wallet picker**: `discoverWallets` finds every BRC-100 wallet that announces itself, plus the in-app wallet, optional localhost desktop wallets and phone pairing. `pickWallet` shows the picker UI. `handcashAdapter` is a transitional slot that wraps an app's own HandCash login and imports no HandCash SDK.
29
+ - **Pairing QR modal**: `pairingModal()` and `qrSvg()`, using the bundled `qrcode-generator` (MIT).
30
+ - Tooling: ESLint (`pnpm lint`) and a CI workflow on pull requests.
31
+
32
+ ## 0.3.0
33
+
34
+ - bApp shell: the `bapp.json` manifest, 5-slot bar, top bar, drawer and in-wallet bridge.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @bwalletx/connect
2
2
 
3
- Add "Sign in with bWalletX" to a website.
3
+ Add "Sign in with bWalletX" to a website. Built on the open BRC-100 wallet interface, so every BRC-100 wallet works the same way, with bWalletX first.
4
4
 
5
5
  The user's wallet signs a one-time challenge from your server with a key derived from its identity key. Your server checks the signature and signs the user in as that identity key. You don't need an API key or an account with us, and nothing passes through bwalletx.com. The only service of ours involved is the encrypted pairing relay (`relay.bwallet.space`), and only when the user signs in from a phone or the web wallet.
6
6
 
@@ -14,6 +14,144 @@ pnpm add @bwalletx/connect @bsv/sdk
14
14
 
15
15
  `@bsv/sdk` (2.2 or later, 2.x or 3.x) is a peer dependency.
16
16
 
17
+ ## Next.js quick start (App Router)
18
+
19
+ Works with bWalletX and any other BRC-100 wallet. Set a session secret (32+ characters) in your environment. There is no default: the routes throw without it.
20
+
21
+ ```sh
22
+ # .env.local (never commit it)
23
+ BWALLETX_SESSION_SECRET= # e.g. the output of: openssl rand -base64 48
24
+ APP_ORIGIN=https://example.com
25
+ ```
26
+
27
+ ```ts
28
+ // lib/bwalletx.ts
29
+ import { createBwalletXAuth, pgStore, signInOrLink } from '@bwalletx/connect/next';
30
+ import { query } from './db'; // (sql, params) => Promise<{ rows }>
31
+
32
+ export const auth = createBwalletXAuth({
33
+ origin: process.env.APP_ORIGIN!,
34
+ store: pgStore({ query }), // or redisStore({ client: upstash })
35
+ // Optional: link the identity key to your user table.
36
+ onVerified: async (identityKey) => {
37
+ const r = await signInOrLink(identityKey, {
38
+ findUserByIdentityKey: async (k) => (await query('select user_id from wallet_links where identity_key = $1', [k])).rows[0]?.user_id ?? null,
39
+ attach: async (uid, k) => void (await query('insert into wallet_links (user_id, identity_key) values ($1, $2)', [uid, k])),
40
+ createUser: async () => (await query('insert into users default values returning id', [])).rows[0].id,
41
+ });
42
+ return { uid: r.userId ?? undefined };
43
+ },
44
+ });
45
+ ```
46
+
47
+ ```ts
48
+ // app/api/bwalletx/challenge/route.ts
49
+ import { auth } from '@/lib/bwalletx';
50
+ export const POST = auth.challenge;
51
+ // app/api/bwalletx/verify/route.ts → export const POST = auth.verify;
52
+ // app/api/bwalletx/logout/route.ts → export const POST = auth.logout;
53
+ // app/api/bwalletx/session/route.ts → export const GET = auth.session;
54
+ ```
55
+
56
+ ```tsx
57
+ // any server component or route handler
58
+ const session = await auth.getSession(); // { sub: identityKey, uid?, handle?, paymail?, exp } | null
59
+ ```
60
+
61
+ The cookie (`bwx_session`) is `HttpOnly; SameSite=Lax; Secure` (Secure when the origin is https), HMAC-SHA256 signed, 7 days by default (`maxAge`). Both POST routes check the `Origin` header against `origin`. `signSession` / `verifySession` are also exported from `@bwalletx/connect/server` if you want the token without the adapter.
62
+
63
+ Create the nonce table once (a migration): `pgNonceTableSql()` returns the SQL.
64
+
65
+ ## React quick start
66
+
67
+ ```tsx
68
+ 'use client';
69
+ import { BwalletXProvider, useBwalletX } from '@bwalletx/connect/react';
70
+
71
+ export function Providers({ children }: { children: React.ReactNode }) {
72
+ return (
73
+ <BwalletXProvider
74
+ challengeUrl="/api/bwalletx/challenge"
75
+ verifyUrl="/api/bwalletx/verify"
76
+ logoutUrl="/api/bwalletx/logout"
77
+ sessionUrl="/api/bwalletx/session">
78
+ {children}
79
+ </BwalletXProvider>
80
+ );
81
+ }
82
+
83
+ function Account() {
84
+ const { status, identityKey, handle, connect, disconnect, tip } = useBwalletX();
85
+ if (status !== 'connected') return <button onClick={() => connect()}>Connect wallet</button>;
86
+ return (
87
+ <>
88
+ <span>{handle ?? identityKey?.slice(0, 10)}</span>
89
+ <button onClick={() => tip({ to: 'author@example.com', usd: 0.05 })}>Tip 5¢</button>
90
+ <button onClick={disconnect}>Sign out</button>
91
+ </>
92
+ );
93
+ }
94
+ ```
95
+
96
+ `useBwalletX()` gives `status` (`idle | restoring | connecting | connected | error`), `identityKey`, `handle`, `paymail`, `wallet` (the BRC-100 `WalletInterface`), `walletId`, `signedIn`, `error`, `wallets`, `connect({ walletId? })`, `disconnect()`, `listWallets()`, `pay`, `tip` and `buyToken`.
97
+
98
+ `connect()` finds wallets, shows the built-in picker when there is more than one (bWalletX first), shows the pairing QR modal for phones, and signs in through your routes when `challengeUrl` and `verifyUrl` are set. Nothing touches `window` during render, so the provider is safe in server-rendered pages. After a reload the session comes back from `sessionUrl`, but `wallet` stays `null` until the user calls `connect()` again.
99
+
100
+ ## Payments
101
+
102
+ ```ts
103
+ import { pay, tip, buyToken } from '@bwalletx/connect';
104
+
105
+ await pay(wallet, { to: 'alice@example.com', sats: 5000, memo: 'Album' }); // paymail
106
+ await pay(wallet, { to: '1Abc...', usd: 1 }); // address; WhatsOnChain rate, or pass usdPerBsv
107
+ const r = await pay(wallet, { to: identityKeyHex, sats: 100 }); // BRC-29; r.remittance goes to the recipient
108
+ await buyToken(wallet, { token: '$BOASE', amount: 100, quote: (q) => fetch('/api/quote', { method: 'POST', body: JSON.stringify(q) }).then((x) => x.json()) });
109
+ ```
110
+
111
+ Each call makes one `createAction`, and nothing is ever retried. Failures throw a `PaymentError` with a `code` (`BAD_REQUEST`, `RESOLVE_FAILED`, `RATE_FAILED`, `WALLET_REJECTED`, `WALLET_ERROR`, `NOT_SUPPORTED`). When `maybeSent` is true, the wallet may already have broadcast the payment, so check before you ask the user again. Browsers often can't resolve paymails themselves because of CORS, so pass `resolvePaymail` to do it on your server.
112
+
113
+ On the server, check the payment before you deliver anything:
114
+
115
+ ```ts
116
+ import { verifyPayment } from '@bwalletx/connect/server';
117
+
118
+ const r = await verifyPayment({
119
+ tx: body.tx ?? body.txid, // Atomic BEEF / BEEF / raw hex, or a txid (fetched from WhatsOnChain)
120
+ expected: [{ address: SELLER_ADDRESS, satoshis: price }], // or { script: hex, satoshis }
121
+ requireSeen: true, // status from WhatsOnChain; or status: 'arc', arcUrl, arcApiKey
122
+ });
123
+ ```
124
+
125
+ Every expected output has to be matched by a different output that carries at least the expected amount. Otherwise `verifyPayment` throws a `PaymentVerificationError` with a `code` (`OUTPUT_MISSING`, `TX_NOT_FOUND`, `NOT_SEEN`, `REJECTED`, `SPV_FAILED`, `BAD_TX`, `NETWORK`). Pass `spv: chainTracker` to check the merkle proofs in a BEEF as well.
126
+
127
+ ## Wallets and the picker
128
+
129
+ `discoverWallets()` lists everything it can find, with bWalletX first:
130
+
131
+ - every wallet that announces itself through BRC-100 discovery (bWalletX, Yours, Metanet and others)
132
+ - the in-app browser's `window.CWI`
133
+ - desktop wallets on localhost ports 3321 and 3301 (only with `probeLocal: true`)
134
+ - phone pairing (`pairing: pairingModal()`)
135
+ - any adapters you add in `extra`
136
+
137
+ `pickWallet(list)` shows the picker. `pairingModal()` returns `PairingOptions` that draw the QR code and match code, so you no longer need your own QR library.
138
+
139
+ ## Migrating from HandCash Connect
140
+
141
+ 1. Keep your HandCash login for now and put it behind the same picker:
142
+ ```ts
143
+ import { handcashAdapter } from '@bwalletx/connect';
144
+ const handcash = handcashAdapter({
145
+ login: () => myExistingHandCashLogin(), // resolves { handle, paymail? } once signed in
146
+ pay: (req) => fetch('/api/handcash/pay', { method: 'POST', body: JSON.stringify(req) }).then((r) => r.json()),
147
+ });
148
+ <BwalletXProvider extraWallets={[handcash]} ... />
149
+ ```
150
+ **This adapter is transitional.** It wraps your own HandCash code; this package does not import any HandCash SDK. HandCash announced BRC-100 support in October 2026. Once that ships, HandCash shows up as an ordinary BRC-100 wallet and you can delete the adapter.
151
+ 2. Let existing users link their wallet. When someone already signed in with HandCash connects bWalletX, call `signInOrLink(identityKey, { currentUserId, ... })` (or `linkIdentity`) in `onVerified`. This links the identity to their existing account instead of creating a new one. If the identity is already linked to someone else, `IdentityLinkConflictError` turns into a 409 response.
152
+ 3. Replace HandCash payment calls with `pay` / `tip` / `buyToken`, and check them on the server with `verifyPayment` instead of HandCash payment webhooks.
153
+ 4. Remove the adapter when your users have moved over.
154
+
17
155
  ## The button
18
156
 
19
157
  `<bwalletx-signin>` is the standard "Sign in with bWalletX" button: a web component with no dependencies (Shadow DOM), plus a React wrapper. It is the gold button from bit-sign's sign-in card.
@@ -108,7 +246,10 @@ interface NonceStore {
108
246
 
109
247
  `take` must read and delete in one step. `verifySignIn` calls it before checking anything else, so each nonce gets exactly one attempt.
110
248
 
111
- `memoryStore()` keeps challenges in the process. It is lost on restart and isn't shared between processes or serverless instances, so it prints a warning the first time it is used. If you leave out `store`, a shared in-memory store is used and you get the same warning. In production, use Redis (`GETDEL`) or a database row you delete with `DELETE ... RETURNING`.
249
+ `memoryStore()` keeps challenges in the process. It is lost on restart and isn't shared between processes or serverless instances, so it prints a warning the first time it is used. If you leave out `store`, a shared in-memory store is used and you get the same warning. In production it warns louder. Use one of the built-in shared stores, which need no driver dependency because you pass your own client:
250
+
251
+ - `redisStore({ client })`: Upstash, Vercel KV or node-redis as they are, or ioredis with `flavor: 'ioredis'`. It uses `GETDEL`, so you need Redis 6.2 or later.
252
+ - `pgStore({ query })`: any `(sql, params)` function (node-postgres, postgres.js, Neon). It deletes each nonce with `DELETE ... RETURNING`, and `pgNonceTableSql()` gives you the table.
112
253
 
113
254
  Also rate-limit the challenge route per IP. It is unauthenticated.
114
255
 
@@ -238,10 +379,10 @@ API: `validateManifest(json)` → `{ok, manifest, errors}`, `parseManifest`, `lo
238
379
 
239
380
  ## Limits
240
381
 
241
- - You get the identity key and nothing else: no handle, paymail or name.
382
+ - Sign-in proves the identity key and nothing else: no handle, paymail or name. Use `onVerified` to look them up and add them yourself.
242
383
  - The identity key is the same on every site that uses this method, so two sites can tell it is the same wallet.
243
384
  - `relay.bwallet.space` is run by The Bitcoin Corporation. It has no published uptime guarantee and allows about 60 frames a minute per channel.
244
- - There is no hosted sign-in page or OAuth-style redirect. You run the two routes yourself.
385
+ - There is no hosted sign-in page or OAuth-style redirect (auth.bwalletx.com is planned). You run the routes yourself; `@bwalletx/connect/next` gives you ready-made ones.
245
386
 
246
387
  ## Testing
247
388
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,118 @@
1
+ import { WalletProtocol } from '@bsv/sdk';
2
+
3
+ /**
4
+ * $401 / $402 / $403 access protocol, version 1. Shared by sites (client + server) and wallets.
5
+ * The full spec is docs/ACCESS-PROTOCOL.md. Pure: no I/O, @bsv/sdk types only.
6
+ *
7
+ * 401 identity "prove who you are (at identity level N, or KYC'd)"
8
+ * 402 payment "pay for this" (the challenge format is reserved; payment itself uses pay())
9
+ * 403 access "prove you hold N of token X (or N basis points of its supply)"
10
+ */
11
+
12
+ declare const ACCESS_VERSION: 1;
13
+ /** Security level 2, a protocol name used for nothing else. Wallets recognise it and show the kind's video. */
14
+ declare const ACCESS_PROTOCOL: WalletProtocol;
15
+ /** How long a challenge is valid. */
16
+ declare const ACCESS_CHALLENGE_LIFETIME_S = 120;
17
+ /** At most this many holding proofs are read from one response. */
18
+ declare const MAX_HOLDING_PROOFS = 50;
19
+ type AccessKind = '401' | '402' | '403';
20
+ interface TokenRequirement {
21
+ /** 'bsv21:<txid>_<vout>' */
22
+ id: string;
23
+ /** Minimum raw (unscaled, integer string) amount. */
24
+ min?: string;
25
+ /** Minimum in basis points of total supply (1 = 0.01%, 10000 = 100%). */
26
+ minBps?: number;
27
+ }
28
+ interface AccessRequirement {
29
+ token?: TokenRequirement;
30
+ /** $401 identity level (bit-sign strands): 1 Basic, 2 Verified, 3 Strong, 4 Sovereign (Veriff). */
31
+ identityLevel?: number;
32
+ /** A live KYC credential is required. */
33
+ kyc?: boolean;
34
+ }
35
+ /** What the wallet signs, as canonical JSON. Times are seconds since the epoch. */
36
+ interface AccessChallenge {
37
+ v: 1;
38
+ kind: AccessKind;
39
+ origin: string;
40
+ nonce: string;
41
+ iat: number;
42
+ exp: number;
43
+ /** The identity key the site expects. A different wallet account is `wrong_account`. */
44
+ sub?: string;
45
+ require: AccessRequirement;
46
+ }
47
+ /** A key that locks some of the required token, proving control by signing the same challenge bytes. */
48
+ interface HoldingProof {
49
+ /** Compressed public key hex of the key locking the outputs. */
50
+ pubkey: string;
51
+ /** DER signature by that key over SHA-256(challenge bytes), as BRC-100 createSignature returns. */
52
+ signature: number[];
53
+ /** Optional hint: outpoints ('txid_vout') this key holds. Servers must not trust it. */
54
+ outpoints?: string[];
55
+ }
56
+ /** What the wallet (via requestAccess) posts back to the site. */
57
+ interface AccessResponse {
58
+ v: 1;
59
+ /** The challenge string exactly as issued. */
60
+ challenge: string;
61
+ identityKey: string;
62
+ /** DER signature: createSignature({ protocolID: ACCESS_PROTOCOL, keyID: nonce, counterparty: 'anyone', data: challenge bytes }). */
63
+ signature: number[];
64
+ holdings?: HoldingProof[];
65
+ }
66
+ type AccessErrorCode = 'not_installed' | 'user_rejected' | 'wrong_account' | 'insufficient_tokens' | 'identity_level_too_low' | 'kyc_required' | 'indexer_unavailable' | 'expired' | 'replay' | 'invalid_signature' | 'bad_request';
67
+ /** The HTTP status a site should answer each code with. */
68
+ declare const ACCESS_ERROR_STATUS: Record<AccessErrorCode, number>;
69
+ interface AccessErrorBody {
70
+ error: AccessErrorCode;
71
+ message: string;
72
+ /** insufficient_tokens: raw amounts; identity_level_too_low: levels. */
73
+ have?: string | number;
74
+ need?: string | number;
75
+ }
76
+ declare class AccessError extends Error {
77
+ readonly code: AccessErrorCode;
78
+ readonly have?: string | number;
79
+ readonly need?: string | number;
80
+ constructor(code: AccessErrorCode, message?: string, extra?: {
81
+ have?: string | number;
82
+ need?: string | number;
83
+ });
84
+ toJSON(): AccessErrorBody;
85
+ }
86
+ /** One video + poster + colour per kind, so wallets and sites show the same thing. Paths are inside the package. */
87
+ declare const PROTOCOL_MEDIA: Record<AccessKind, {
88
+ name: string;
89
+ color: string;
90
+ video: string;
91
+ poster: string;
92
+ }>;
93
+ /** Media URLs on a CDN (unpkg) for a given package version. */
94
+ declare function protocolMediaUrl(kind: AccessKind, which: 'video' | 'poster', version?: string): string;
95
+ /**
96
+ * Canonical JSON: object keys sorted by code unit, no whitespace, undefined members dropped.
97
+ * Only strings, finite numbers, booleans, null, arrays and plain objects are allowed.
98
+ */
99
+ declare function canonicalJson(v: unknown): string;
100
+ /** The bytes every signature in the protocol covers: UTF-8 of the challenge string. */
101
+ declare function challengeBytes(challenge: string): number[];
102
+ /** Throws AccessError('bad_request') if the requirement is malformed. Returns a normalised copy. */
103
+ declare function normaliseRequirement(r: unknown): AccessRequirement;
104
+ /** The kind a requirement implies: any token → 403; otherwise 401. */
105
+ declare function kindFor(r: AccessRequirement): AccessKind;
106
+ /** Parse and validate a challenge string. Throws AccessError('bad_request'). Does not check expiry. */
107
+ declare function parseAccessChallenge(s: unknown): AccessChallenge;
108
+ /** Raw amount needed for a token requirement, given total supply (needed only for minBps). Rounds up. */
109
+ declare function tokenNeedRaw(t: TokenRequirement, supplyRaw?: string): string;
110
+ /** Compare unscaled integer strings. Fails closed on malformed input. */
111
+ declare function atLeastRaw(have: string, need: string): boolean;
112
+ /** A user-readable line for an error code. */
113
+ declare function accessErrorMessage(code: AccessErrorCode, e?: {
114
+ have?: string | number;
115
+ need?: string | number;
116
+ }): string;
117
+
118
+ export { AccessError as A, type HoldingProof as H, MAX_HOLDING_PROOFS as M, PROTOCOL_MEDIA as P, type TokenRequirement as T, type AccessRequirement as a, type AccessResponse as b, ACCESS_ERROR_STATUS as c, ACCESS_PROTOCOL as d, ACCESS_VERSION as e, type AccessChallenge as f, type AccessErrorBody as g, type AccessErrorCode as h, type AccessKind as i, accessErrorMessage as j, canonicalJson as k, ACCESS_CHALLENGE_LIFETIME_S as l, atLeastRaw as m, challengeBytes as n, kindFor as o, protocolMediaUrl as p, normaliseRequirement as q, parseAccessChallenge as r, tokenNeedRaw as t };
@@ -0,0 +1,118 @@
1
+ import { WalletProtocol } from '@bsv/sdk';
2
+
3
+ /**
4
+ * $401 / $402 / $403 access protocol, version 1. Shared by sites (client + server) and wallets.
5
+ * The full spec is docs/ACCESS-PROTOCOL.md. Pure: no I/O, @bsv/sdk types only.
6
+ *
7
+ * 401 identity "prove who you are (at identity level N, or KYC'd)"
8
+ * 402 payment "pay for this" (the challenge format is reserved; payment itself uses pay())
9
+ * 403 access "prove you hold N of token X (or N basis points of its supply)"
10
+ */
11
+
12
+ declare const ACCESS_VERSION: 1;
13
+ /** Security level 2, a protocol name used for nothing else. Wallets recognise it and show the kind's video. */
14
+ declare const ACCESS_PROTOCOL: WalletProtocol;
15
+ /** How long a challenge is valid. */
16
+ declare const ACCESS_CHALLENGE_LIFETIME_S = 120;
17
+ /** At most this many holding proofs are read from one response. */
18
+ declare const MAX_HOLDING_PROOFS = 50;
19
+ type AccessKind = '401' | '402' | '403';
20
+ interface TokenRequirement {
21
+ /** 'bsv21:<txid>_<vout>' */
22
+ id: string;
23
+ /** Minimum raw (unscaled, integer string) amount. */
24
+ min?: string;
25
+ /** Minimum in basis points of total supply (1 = 0.01%, 10000 = 100%). */
26
+ minBps?: number;
27
+ }
28
+ interface AccessRequirement {
29
+ token?: TokenRequirement;
30
+ /** $401 identity level (bit-sign strands): 1 Basic, 2 Verified, 3 Strong, 4 Sovereign (Veriff). */
31
+ identityLevel?: number;
32
+ /** A live KYC credential is required. */
33
+ kyc?: boolean;
34
+ }
35
+ /** What the wallet signs, as canonical JSON. Times are seconds since the epoch. */
36
+ interface AccessChallenge {
37
+ v: 1;
38
+ kind: AccessKind;
39
+ origin: string;
40
+ nonce: string;
41
+ iat: number;
42
+ exp: number;
43
+ /** The identity key the site expects. A different wallet account is `wrong_account`. */
44
+ sub?: string;
45
+ require: AccessRequirement;
46
+ }
47
+ /** A key that locks some of the required token, proving control by signing the same challenge bytes. */
48
+ interface HoldingProof {
49
+ /** Compressed public key hex of the key locking the outputs. */
50
+ pubkey: string;
51
+ /** DER signature by that key over SHA-256(challenge bytes), as BRC-100 createSignature returns. */
52
+ signature: number[];
53
+ /** Optional hint: outpoints ('txid_vout') this key holds. Servers must not trust it. */
54
+ outpoints?: string[];
55
+ }
56
+ /** What the wallet (via requestAccess) posts back to the site. */
57
+ interface AccessResponse {
58
+ v: 1;
59
+ /** The challenge string exactly as issued. */
60
+ challenge: string;
61
+ identityKey: string;
62
+ /** DER signature: createSignature({ protocolID: ACCESS_PROTOCOL, keyID: nonce, counterparty: 'anyone', data: challenge bytes }). */
63
+ signature: number[];
64
+ holdings?: HoldingProof[];
65
+ }
66
+ type AccessErrorCode = 'not_installed' | 'user_rejected' | 'wrong_account' | 'insufficient_tokens' | 'identity_level_too_low' | 'kyc_required' | 'indexer_unavailable' | 'expired' | 'replay' | 'invalid_signature' | 'bad_request';
67
+ /** The HTTP status a site should answer each code with. */
68
+ declare const ACCESS_ERROR_STATUS: Record<AccessErrorCode, number>;
69
+ interface AccessErrorBody {
70
+ error: AccessErrorCode;
71
+ message: string;
72
+ /** insufficient_tokens: raw amounts; identity_level_too_low: levels. */
73
+ have?: string | number;
74
+ need?: string | number;
75
+ }
76
+ declare class AccessError extends Error {
77
+ readonly code: AccessErrorCode;
78
+ readonly have?: string | number;
79
+ readonly need?: string | number;
80
+ constructor(code: AccessErrorCode, message?: string, extra?: {
81
+ have?: string | number;
82
+ need?: string | number;
83
+ });
84
+ toJSON(): AccessErrorBody;
85
+ }
86
+ /** One video + poster + colour per kind, so wallets and sites show the same thing. Paths are inside the package. */
87
+ declare const PROTOCOL_MEDIA: Record<AccessKind, {
88
+ name: string;
89
+ color: string;
90
+ video: string;
91
+ poster: string;
92
+ }>;
93
+ /** Media URLs on a CDN (unpkg) for a given package version. */
94
+ declare function protocolMediaUrl(kind: AccessKind, which: 'video' | 'poster', version?: string): string;
95
+ /**
96
+ * Canonical JSON: object keys sorted by code unit, no whitespace, undefined members dropped.
97
+ * Only strings, finite numbers, booleans, null, arrays and plain objects are allowed.
98
+ */
99
+ declare function canonicalJson(v: unknown): string;
100
+ /** The bytes every signature in the protocol covers: UTF-8 of the challenge string. */
101
+ declare function challengeBytes(challenge: string): number[];
102
+ /** Throws AccessError('bad_request') if the requirement is malformed. Returns a normalised copy. */
103
+ declare function normaliseRequirement(r: unknown): AccessRequirement;
104
+ /** The kind a requirement implies: any token → 403; otherwise 401. */
105
+ declare function kindFor(r: AccessRequirement): AccessKind;
106
+ /** Parse and validate a challenge string. Throws AccessError('bad_request'). Does not check expiry. */
107
+ declare function parseAccessChallenge(s: unknown): AccessChallenge;
108
+ /** Raw amount needed for a token requirement, given total supply (needed only for minBps). Rounds up. */
109
+ declare function tokenNeedRaw(t: TokenRequirement, supplyRaw?: string): string;
110
+ /** Compare unscaled integer strings. Fails closed on malformed input. */
111
+ declare function atLeastRaw(have: string, need: string): boolean;
112
+ /** A user-readable line for an error code. */
113
+ declare function accessErrorMessage(code: AccessErrorCode, e?: {
114
+ have?: string | number;
115
+ need?: string | number;
116
+ }): string;
117
+
118
+ export { AccessError as A, type HoldingProof as H, MAX_HOLDING_PROOFS as M, PROTOCOL_MEDIA as P, type TokenRequirement as T, type AccessRequirement as a, type AccessResponse as b, ACCESS_ERROR_STATUS as c, ACCESS_PROTOCOL as d, ACCESS_VERSION as e, type AccessChallenge as f, type AccessErrorBody as g, type AccessErrorCode as h, type AccessKind as i, accessErrorMessage as j, canonicalJson as k, ACCESS_CHALLENGE_LIFETIME_S as l, atLeastRaw as m, challengeBytes as n, kindFor as o, protocolMediaUrl as p, normaliseRequirement as q, parseAccessChallenge as r, tokenNeedRaw as t };