@sigma-auth/better-auth-plugin 0.0.82 → 0.0.84

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/README.md CHANGED
@@ -1,150 +1,70 @@
1
1
  # @sigma-auth/better-auth-plugin
2
2
 
3
- Better Auth plugins for Sigma Identity - Bitcoin-native authentication with BAP identity support.
3
+ ![Sigma Auth Bitcoin-Native Authentication for Better Auth](assets/banner.png)
4
4
 
5
- ## Installation
6
-
7
- ### Package
8
-
9
- ```bash
10
- bun add @sigma-auth/better-auth-plugin
11
- # or
12
- npm install @sigma-auth/better-auth-plugin
13
- ```
5
+ Bitcoin-native authentication for Better Auth. Users sign in with their Bitcoin wallet. Identity is a cryptographic keypair — persistent across apps, controlled only by the user.
14
6
 
15
- ### Claude Code Plugin
7
+ Maintained by [Sigma Identity](https://sigmaidentity.com). For support, open an issue on [GitHub](https://github.com/b-open-io/better-auth-plugin/issues).
16
8
 
17
- Install the Claude Code plugin for AI-assisted setup, diagnostics, and troubleshooting:
9
+ ## Features
18
10
 
19
- ```bash
20
- claude plugin install sigma-auth@b-open-io
21
- ```
11
+ - **Passwordless from day one** — Bitcoin wallet signatures replace passwords and magic links
12
+ - **BAP identity support** — Bitcoin Attestation Protocol provides persistent, portable user profiles
13
+ - **PKCE OAuth flow** — Fully spec-compliant authorization code flow with PKCE for public clients
14
+ - **Iframe signer** — Client-side signing without exposing private keys to your app domain
15
+ - **Local signer fallback** — Optional local TokenPass server for offline or privacy-first deployments
16
+ - **NFT-based role gating** — Assign roles based on NFT collection ownership across connected wallets
17
+ - **Token balance gating** — Grant roles when users hold minimum BSV-21 token balances
18
+ - **BAP admin whitelist** — Designate admins by Bitcoin identity key, checked at every session creation
19
+ - **Multi-identity wallets** — Users can select among multiple BAP identities at sign-in
20
+ - **Subscription tiers** — Verified subscription status flows through to your session
21
+ - **Next.js App Router** — Ready-to-use route handlers with a single import
22
+ - **Payload CMS** — Drop-in callback handler with customizable user creation
23
+ - **Convex** — Works inside `@convex-dev/better-auth` with no local auth instance required
24
+ - **Full TypeScript** — All types exported, including `SigmaUserInfo`, `BAPProfile`, and JWT claims
22
25
 
23
- Or install skills individually:
26
+ ## Installation
24
27
 
25
28
  ```bash
26
- npx add-skill b-open-io/sigma-auth-better-auth-plugin
29
+ bun add @sigma-auth/better-auth-plugin
30
+ # or
31
+ npm install @sigma-auth/better-auth-plugin
27
32
  ```
28
33
 
29
- #### Available Skills
34
+ ### Peer dependencies
30
35
 
31
- | Skill | Command | Description |
32
- |-------|---------|-------------|
33
- | **setup-nextjs** | `/sigma-auth:setup-nextjs` | Step-by-step Next.js integration guide with project detection, env validation, and health check scripts |
34
- | **setup-convex** | `/sigma-auth:setup-convex` | Convex + Better Auth integration guide with server plugin configuration |
35
- | **bitcoin-auth-diagnostics** | `/sigma-auth:bitcoin-auth-diagnostics` | Diagnose token verification failures, signature errors, and integration issues |
36
- | **tokenpass** | `/sigma-auth:tokenpass` | Token-gated access patterns using NFT ownership verification |
37
- | **device-authorization** | `/sigma-auth:device-authorization` | Device authorization flow for CLI tools and IoT devices |
38
-
39
- #### Setup Scripts (via setup-nextjs skill)
36
+ Install only what you use:
40
37
 
41
38
  ```bash
42
- # Analyze your project and get setup recommendations
43
- bun run skills/setup-nextjs/scripts/detect.ts /path/to/project
44
-
45
- # Validate environment variables
46
- bun run skills/setup-nextjs/scripts/validate-env.ts
47
-
48
- # Test connection to Sigma Auth server
49
- bun run skills/setup-nextjs/scripts/health-check.ts
50
- ```
51
-
52
- ## How It Works
53
-
54
- The plugin runs **inside your app**, not on the Sigma server. It handles OAuth token exchange and Better Auth integration:
55
-
56
- ```
57
- ┌─────────────────────────────────────────────────────────────────┐
58
- │ Your App (runs on your server/Vercel) │
59
- │ │
60
- │ ┌─────────────────────┐ ┌──────────────────────────────┐ │
61
- │ │ auth-server.ts │ │ /api/auth/sigma/callback │ │
62
- │ │ - betterAuth() │◄───│ - createBetterAuthCallback │ │
63
- │ │ - sigmaProvider() │ │ Handler({ auth }) │ │
64
- │ └─────────────────────┘ └──────────────────────────────┘ │
65
- │ │ │ │
66
- │ ▼ ▼ │
67
- │ ┌─────────────────────┐ ┌──────────────────────────────┐ │
68
- │ │ Your Database │ │ @sigma-auth/better-auth- │ │
69
- │ │ - users │◄───│ plugin (this package) │ │
70
- │ │ - accounts │ │ │ │
71
- │ │ - sessions │ │ Exchanges code for tokens, │ │
72
- │ └─────────────────────┘ │ creates users/accounts/ │ │
73
- │ │ sessions in YOUR database │ │
74
- │ └──────────────────────────────┘ │
75
- └─────────────────────────────────────────────────────────────────┘
76
-
77
- │ OAuth flow
78
-
79
- ┌──────────────────────────────────┐
80
- │ auth.sigmaidentity.com │
81
- │ (External Sigma Auth Server) │
82
- │ - /oauth2/authorize │
83
- │ - /oauth2/token │
84
- │ - /oauth2/userinfo │
85
- └──────────────────────────────────┘
86
- ```
87
-
88
- ## Entry Points
89
-
90
- This package provides multiple entry points for different use cases:
91
-
92
- - **`/client`** - Browser-side OAuth client with PKCE
93
- - **`/server`** - Server-side utilities for token exchange
94
- - **`/next`** - Next.js API route handlers
95
- - **`/payload`** - Payload CMS integration with session management
96
- - **`/provider`** - Better Auth server plugin for OIDC provider
97
-
98
- ## Architecture
99
-
100
- ### OAuth Flow (Cross-Domain)
101
-
102
- When your app authenticates with Sigma Identity (or another Better Auth server on a different domain), you use OAuth/OIDC flow with tokens:
103
-
104
- 1. User clicks sign in → redirects to `auth.sigmaidentity.com`
105
- 2. User authenticates with Bitcoin wallet
106
- 3. Redirects back to your app with authorization code
107
- 4. Your backend exchanges code for access tokens
108
- 5. **Store user data and tokens locally** (Context, Zustand, localStorage, etc.)
39
+ # Required for all integrations
40
+ bun add better-auth
109
41
 
110
- **Important:** Cross-domain cookies don't work due to browser security. Better Auth's `useSession` hook only works when the auth server is on the **same domain** as your app. For OAuth clients, you manage authentication state locally with tokens.
42
+ # Required for server-side token exchange
43
+ bun add bitcoin-auth
111
44
 
112
- ### Wallet Unlock Gate
45
+ # Required for the provider plugin (running your own auth server)
46
+ bun add @bsv/sdk bsv-bap @neondatabase/serverless zod
113
47
 
114
- This plugin fronts Better Auth's OIDC authorize endpoint to ensure wallet access is a prerequisite to authentication.
115
-
116
- The client redirects to `/oauth2/authorize` (custom gate) instead of `/api/auth/oauth2/authorize` (Better Auth directly). The gate checks:
117
-
118
- 1. **Session** - If authenticated, proceed immediately
119
- 2. **Local backup** - If encrypted backup exists, prompt for password
120
- 3. **Cloud backup** - If available, redirect to restore
121
- 4. **Signup** - No backup found, create new account
122
-
123
- This makes Bitcoin identity the foundation of authentication.
124
-
125
- ## Choose Your Integration Mode
126
-
127
- - **Mode A — OAuth client (cross-domain):** Your app is not the auth server. You handle tokens locally.
128
- - **Mode B — Same-domain Better Auth server:** You run Better Auth (or proxy to Convex) on your domain and can use sessions.
48
+ # Required for Payload CMS integration
49
+ bun add payload-auth
50
+ ```
129
51
 
130
- ## Quick Start (Mode A: OAuth Client)
52
+ ## Quick Start
131
53
 
132
- This is the standard setup for apps authenticating with Sigma Identity.
54
+ This is the standard setup for an app that authenticates users via Sigma Identity. The whole flow takes under five minutes.
133
55
 
134
- ### 1. Environment Variables
56
+ ### 1. Set environment variables
135
57
 
136
58
  ```bash
137
- # Your registered OAuth client ID
138
- NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app
139
-
140
- # Member private key for signing token exchange requests (server-side only)
141
- SIGMA_MEMBER_PRIVATE_KEY=your-member-wif
142
-
143
- # Sigma Auth server URL
59
+ # .env.local
60
+ NEXT_PUBLIC_SIGMA_CLIENT_ID=your-app-id
144
61
  NEXT_PUBLIC_SIGMA_AUTH_URL=https://auth.sigmaidentity.com
62
+ SIGMA_MEMBER_PRIVATE_KEY=your-wif-private-key # server-side only
145
63
  ```
146
64
 
147
- ### 2. Create Auth Client
65
+ Get your client ID and register your redirect URI at [sigmaidentity.com/developers](https://sigmaidentity.com/developers).
66
+
67
+ ### 2. Configure the auth client
148
68
 
149
69
  ```typescript
150
70
  // lib/auth.ts
@@ -155,14 +75,9 @@ export const authClient = createAuthClient({
155
75
  baseURL: process.env.NEXT_PUBLIC_SIGMA_AUTH_URL || "https://auth.sigmaidentity.com",
156
76
  plugins: [sigmaClient()],
157
77
  });
158
-
159
- // Export sign in method for OAuth flow
160
- export const signIn = authClient.signIn;
161
78
  ```
162
79
 
163
- ### 3. Token Exchange API Route
164
-
165
- This server-side endpoint exchanges the OAuth code for tokens.
80
+ ### 3. Add the token exchange route
166
81
 
167
82
  ```typescript
168
83
  // app/api/auth/sigma/callback/route.ts
@@ -172,134 +87,121 @@ export const runtime = "nodejs";
172
87
  export const POST = createCallbackHandler();
173
88
  ```
174
89
 
175
- ### 4. OAuth Callback Page
176
-
177
- This page handles the OAuth redirect and stores the authenticated user.
90
+ ### 4. Add the OAuth callback page
178
91
 
179
92
  ```typescript
180
93
  // app/auth/sigma/callback/page.tsx
181
94
  "use client";
182
95
 
183
- import { Suspense, useEffect, useState } from "react";
96
+ import { Suspense, useEffect } from "react";
184
97
  import { useRouter, useSearchParams } from "next/navigation";
185
98
  import { authClient } from "@/lib/auth";
186
99
 
187
100
  function CallbackContent() {
188
101
  const router = useRouter();
189
102
  const searchParams = useSearchParams();
190
- const [error, setError] = useState<string | null>(null);
191
103
 
192
104
  useEffect(() => {
193
- const handleCallback = async () => {
194
- try {
195
- // Exchange code for tokens and get user data
196
- const result = await authClient.sigma.handleCallback(searchParams);
197
-
198
- // Store user data in your app's state management
199
- // Example: Context, Zustand, localStorage, etc.
105
+ authClient.sigma.handleCallback(searchParams)
106
+ .then((result) => {
107
+ // Store tokens and user data in your state management solution
200
108
  localStorage.setItem("sigma_user", JSON.stringify(result.user));
201
109
  localStorage.setItem("sigma_access_token", result.access_token);
202
- localStorage.setItem("sigma_id_token", result.id_token);
203
- if (result.refresh_token) {
204
- localStorage.setItem("sigma_refresh_token", result.refresh_token);
205
- }
206
-
207
- // Redirect to your app
208
110
  router.push("/");
209
- } catch (err: any) {
210
- console.error("OAuth callback error:", err);
211
- setError(err.message || "Authentication failed");
212
- }
213
- };
214
-
215
- handleCallback();
111
+ })
112
+ .catch((err) => {
113
+ authClient.sigma.redirectToError(err);
114
+ });
216
115
  }, [searchParams, router]);
217
116
 
218
- if (error) {
219
- return (
220
- <div className="flex min-h-screen items-center justify-center">
221
- <div className="text-center">
222
- <h2 className="text-xl font-semibold text-red-600">Authentication Failed</h2>
223
- <p className="mt-2 text-sm text-gray-600">{error}</p>
224
- <button
225
- onClick={() => router.push("/")}
226
- className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
227
- >
228
- Return Home
229
- </button>
230
- </div>
231
- </div>
232
- );
233
- }
234
-
235
- return (
236
- <div className="flex min-h-screen items-center justify-center">
237
- <div className="text-center">
238
- <h2 className="text-xl font-semibold">Completing sign in...</h2>
239
- <p className="mt-2 text-sm text-gray-600">Please wait</p>
240
- </div>
241
- </div>
242
- );
117
+ return <p>Completing sign in...</p>;
243
118
  }
244
119
 
245
120
  export default function CallbackPage() {
246
121
  return (
247
- <Suspense fallback={<div>Loading...</div>}>
122
+ <Suspense fallback={<p>Loading...</p>}>
248
123
  <CallbackContent />
249
124
  </Suspense>
250
125
  );
251
126
  }
252
127
  ```
253
128
 
254
- ### 5. Sign In
129
+ ### 5. Add a sign-in button
255
130
 
256
131
  ```typescript
257
- // In your sign-in button component
258
- import { signIn } from "@/lib/auth";
132
+ import { authClient } from "@/lib/auth";
259
133
 
260
- const handleSignIn = () => {
261
- signIn.sigma({
262
- clientId: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID || "your-app",
263
- // callbackURL defaults to /auth/sigma/callback
264
- });
265
- };
134
+ export function SignInButton() {
135
+ return (
136
+ <button
137
+ onClick={() =>
138
+ authClient.signIn.sigma({
139
+ clientId: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID!,
140
+ })
141
+ }
142
+ >
143
+ Sign in with Bitcoin
144
+ </button>
145
+ );
146
+ }
266
147
  ```
267
148
 
268
- ### 6. Access User Data
149
+ The user clicks the button, authenticates with their Bitcoin wallet on the Sigma Identity server, and lands back in your app with a `SigmaUserInfo` object containing their pubkey, display name, and BAP identity.
269
150
 
270
- Since you're managing state locally, access user data from your state management solution:
151
+ ---
271
152
 
272
- ```typescript
273
- // Example with Context
274
- import { createContext, useContext, useEffect, useState } from "react";
153
+ ## Architecture
275
154
 
276
- const AuthContext = createContext<{ user: SigmaUserInfo | null }>({ user: null });
155
+ ### How Bitcoin authentication works
277
156
 
278
- export function AuthProvider({ children }) {
279
- const [user, setUser] = useState<SigmaUserInfo | null>(null);
157
+ Bitcoin auth relies on a cryptographic signature the user produces with a private key that remains in their wallet — the signature is request-specific, timestamped, and covers the request body, so replaying it against a different endpoint or a modified payload fails verification.
280
158
 
281
- useEffect(() => {
282
- const storedUser = localStorage.getItem("sigma_user");
283
- if (storedUser) {
284
- setUser(JSON.parse(storedUser));
285
- }
286
- }, []);
159
+ The flow:
287
160
 
288
- return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>;
289
- }
161
+ 1. Your app redirects to `auth.sigmaidentity.com/oauth2/authorize` with a PKCE challenge
162
+ 2. Sigma's wallet gate checks whether the user has an accessible Bitcoin identity (local backup, cloud backup, or creates one)
163
+ 3. The user signs a challenge with their Bitcoin private key
164
+ 4. Sigma's Better Auth server validates the signature and issues an authorization code
165
+ 5. Your backend exchanges the code for tokens using a Bitcoin-signed request (`X-Auth-Token` header)
166
+ 6. You receive an OIDC `id_token`, an `access_token`, and the user's `SigmaUserInfo` including their `pubkey` and BAP identity
167
+
168
+ The user's identity is their Bitcoin key — stored in their wallet, verifiable cryptographically, and independent of your app's database.
290
169
 
291
- export const useAuth = () => useContext(AuthContext);
170
+ ### Signer architecture
292
171
 
293
- // In components
294
- const { user } = useAuth();
295
- const isAdmin = user?.bap?.idKey === process.env.ADMIN_BAP_ID;
172
+ The plugin supports two signing backends. Both implement the same `SigmaSigner` interface.
173
+
174
+ **Iframe signer (default)** A hidden iframe loads `auth.sigmaidentity.com/signer`. Your app communicates with it via `postMessage`. Private keys stay on the Sigma domain and are never accessible to your JavaScript context. The iframe surfaces a password prompt when the wallet is locked.
175
+
176
+ **Local signer** — An optional [TokenPass](https://tokenpas.app) desktop application running at `http://localhost:21000`. When `preferLocal: true` is set, the client probes for the local server first and falls back to the iframe if unavailable. This enables fully offline signing.
177
+
178
+ ```typescript
179
+ // Prefer local signer with iframe fallback
180
+ const authClient = createAuthClient({
181
+ plugins: [
182
+ sigmaClient({
183
+ preferLocal: true,
184
+ localServerUrl: "http://localhost:21000",
185
+ onServerDetected: (url, isLocal) => {
186
+ console.log(`Using ${isLocal ? "local" : "cloud"} signer: ${url}`);
187
+ },
188
+ }),
189
+ ],
190
+ });
296
191
  ```
297
192
 
298
- ## Alternative: Same-Domain Setup
193
+ ### Integration modes
194
+
195
+ | Mode | When to use | Session management |
196
+ |------|-------------|-------------------|
197
+ | **Mode A — OAuth client** | Your app is separate from the auth server (most apps) | Tokens in localStorage or your state management |
198
+ | **Mode B — Same-domain** | You run Better Auth on the same domain as your app | Session cookies + `useSession` hook |
199
+
200
+ ---
299
201
 
300
- If you run your own Better Auth server on the **same domain** as your app, you can use session cookies and the `useSession` hook. You still need the OAuth callback page (above) to handle the redirect.
202
+ ## Server configuration (Mode B)
301
203
 
302
- ### Server Setup (Next.js)
204
+ When you run Better Auth on the same domain as your app, use `sigmaCallbackPlugin` to handle the OAuth callback inside Better Auth itself. No separate API route is needed.
303
205
 
304
206
  ```typescript
305
207
  // lib/auth.ts
@@ -319,123 +221,257 @@ import { auth } from "@/lib/auth";
319
221
  export const { GET, POST } = toNextJsHandler(auth);
320
222
  ```
321
223
 
224
+ The client stays identical — `sigmaClient()` detects whether the auth server is on the same domain and routes the callback accordingly.
225
+
226
+ ### Options
227
+
322
228
  ```typescript
323
- // lib/auth.ts
324
- export const authClient = createAuthClient({
325
- baseURL: "/api/auth", // Same domain
326
- plugins: [sigmaClient()],
229
+ sigmaCallbackPlugin({
230
+ accountPrivateKey?: string; // Default: SIGMA_MEMBER_PRIVATE_KEY env
231
+ clientId?: string; // Default: NEXT_PUBLIC_SIGMA_CLIENT_ID env
232
+ issuerUrl?: string; // Default: NEXT_PUBLIC_SIGMA_AUTH_URL env
233
+ callbackPath?: string; // Default: "/auth/sigma/callback"
234
+ emailDomain?: string; // Default: "sigma.local"
235
+ })
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Client API reference
241
+
242
+ All methods are available on the object returned by `createAuthClient({ plugins: [sigmaClient()] })`.
243
+
244
+ ### Authentication
245
+
246
+ ```typescript
247
+ // Redirect to Sigma Identity for sign-in
248
+ authClient.signIn.sigma({
249
+ clientId: "your-app",
250
+ callbackURL: "/auth/sigma/callback", // default
251
+ errorCallbackURL: "/auth/sigma/error",
252
+ bapId: "specific-identity-id", // for multi-identity wallets
253
+ prompt: "select_account", // force account selection
254
+ forceLogin: false, // bypass existing session check
327
255
  });
328
256
 
329
- export const { useSession } = authClient;
257
+ // Handle the OAuth redirect in your callback page
258
+ const result = await authClient.sigma.handleCallback(searchParams);
259
+ // result: { user: SigmaUserInfo, access_token, id_token, refresh_token? }
330
260
 
331
- // In components
332
- const { data: session } = useSession();
261
+ // Redirect to error page with structured error params
262
+ authClient.sigma.redirectToError(caughtError);
333
263
  ```
334
264
 
335
- This requires setting up Better Auth server with the `sigmaCallbackPlugin` on your domain.
265
+ ### Identity management
336
266
 
337
- ## Payload CMS Integration
267
+ ```typescript
268
+ // Get the current BAP identity (set automatically after handleCallback)
269
+ const bapId = authClient.sigma.getIdentity(); // string | null
270
+
271
+ // Manually set identity (for multi-identity scenarios)
272
+ authClient.sigma.setIdentity("bap-identity-id");
338
273
 
339
- For Payload CMS apps using [payload-auth](https://github.com/b-open-io/payload-auth), the `/payload` entry point provides a callback handler that automatically:
274
+ // Clear stored identity on logout
275
+ authClient.sigma.clearIdentity();
276
+
277
+ // Check whether signer is ready
278
+ const ready = authClient.sigma.isReady(); // boolean
279
+ ```
340
280
 
341
- 1. Exchanges the authorization code for tokens
342
- 2. Finds or creates a user in your Payload users collection
343
- 3. Creates a better-auth session in Payload's sessions collection
344
- 4. Sets the session cookie
281
+ ### Signing
345
282
 
346
- ### Setup
283
+ The signing keys remain on `auth.sigmaidentity.com` — only the resulting signature string is returned to your JavaScript context.
347
284
 
348
285
  ```typescript
349
- // app/api/auth/sigma/callback/route.ts
350
- import configPromise from "@payload-config";
351
- import { createPayloadCallbackHandler } from "@sigma-auth/better-auth-plugin/payload";
286
+ // Sign an API request (returns X-Auth-Token string)
287
+ const authToken = await authClient.sigma.sign("/api/posts", { title: "Hello" });
288
+ fetch("/api/posts", {
289
+ method: "POST",
290
+ headers: { "X-Auth-Token": authToken },
291
+ body: JSON.stringify({ title: "Hello" }),
292
+ });
352
293
 
353
- export const runtime = "nodejs";
354
- export const POST = createPayloadCallbackHandler({ configPromise });
294
+ // Sign OP_RETURN data for Bitcoin transactions (AIP format)
295
+ const signedOps = await authClient.sigma.signAIP(["6a", "..."]);
296
+
297
+ // Encrypt a message for a specific friend (Type42 key derivation)
298
+ const ciphertext = await authClient.sigma.encrypt(
299
+ "Hello!",
300
+ "friend-bap-id",
301
+ friend.pubkey, // optional
302
+ );
303
+
304
+ // Decrypt a message from a friend
305
+ const plaintext = await authClient.sigma.decrypt(
306
+ ciphertext,
307
+ "friend-bap-id",
308
+ sender.pubkey,
309
+ );
310
+
311
+ // Get your derived public key for a friend (for friend requests)
312
+ const myPubKey = await authClient.sigma.getFriendPublicKey("friend-bap-id");
355
313
  ```
356
314
 
357
- ### Custom User Creation
315
+ ### Signer detection
358
316
 
359
- Override the default user creation to add custom fields:
317
+ ```typescript
318
+ const { url, isLocal } = await authClient.sigma.detectServer();
319
+ const signerType = authClient.sigma.getSignerType(); // "local" | "iframe" | null
320
+ ```
321
+
322
+ ### Wallet management
360
323
 
361
324
  ```typescript
362
- export const POST = createPayloadCallbackHandler({
363
- configPromise,
364
- createUser: async (payload, sigmaUser) => {
365
- return payload.create({
366
- collection: "users",
367
- data: {
368
- email: sigmaUser.email || `${sigmaUser.sub}@sigma.identity`,
369
- name: sigmaUser.name || sigmaUser.sub,
370
- emailVerified: true,
371
- role: ["subscriber"], // Custom role
372
- bapId: sigmaUser.bap_id,
373
- pubkey: sigmaUser.pubkey,
374
- },
375
- });
376
- },
325
+ // Get connected wallets for the current user
326
+ const { wallets } = await authClient.wallet.getConnected();
327
+
328
+ // Connect an additional wallet
329
+ await authClient.wallet.connect(bapId, authToken, "yours");
330
+
331
+ // Disconnect a wallet
332
+ await authClient.wallet.disconnect(bapId, walletAddress);
333
+
334
+ // Set primary wallet (for receiving NFTs)
335
+ await authClient.wallet.setPrimary(bapId, walletAddress);
336
+ ```
337
+
338
+ ### NFT ownership
339
+
340
+ ```typescript
341
+ // List all NFTs across connected wallets
342
+ const { wallets, totalNFTs } = await authClient.nft.list();
343
+
344
+ // Force refresh from blockchain
345
+ const fresh = await authClient.nft.list(true);
346
+
347
+ // Verify ownership of a collection or specific origin
348
+ const { owns, count } = await authClient.nft.verifyOwnership({
349
+ collection: "collection-id",
350
+ minCount: 1,
377
351
  });
378
352
  ```
379
353
 
380
- ### Configuration Options
354
+ ### Subscriptions
381
355
 
382
356
  ```typescript
383
- interface PayloadCallbackConfig {
384
- /** Payload config promise (required) */
385
- configPromise: Promise<unknown>;
357
+ // Get subscription status based on NFT ownership
358
+ const status = await authClient.subscription.getStatus();
359
+ // status: { tier: "free" | "plus" | "pro" | "premium" | "enterprise", isActive, features }
386
360
 
387
- /** Sigma Auth server URL (default: NEXT_PUBLIC_SIGMA_AUTH_URL) */
388
- issuerUrl?: string;
361
+ // Check if current tier meets a minimum requirement
362
+ const hasAccess = authClient.subscription.hasTier(status.tier, "pro");
363
+ ```
389
364
 
390
- /** OAuth client ID (default: NEXT_PUBLIC_SIGMA_CLIENT_ID) */
391
- clientId?: string;
365
+ ---
392
366
 
393
- /** Member private key (default: SIGMA_MEMBER_PRIVATE_KEY env) */
394
- memberPrivateKey?: string;
367
+ ## Database schema
395
368
 
396
- /** Callback path (default: /auth/sigma/callback) */
397
- callbackPath?: string;
369
+ The plugin adds the following fields to your Better Auth schema. Run `bunx better-auth generate` after adding the plugin to get the migration.
398
370
 
399
- /** Users collection slug (default: "users") */
400
- usersCollection?: string;
371
+ ### `user` table additions
401
372
 
402
- /** Sessions collection slug (default: "sessions") */
403
- sessionsCollection?: string;
373
+ | Column | Type | Description |
374
+ |--------|------|-------------|
375
+ | `pubkey` | `string` (unique, required) | Bitcoin public key for this user |
376
+ | `subscriptionTier` | `string` | Subscription tier (`free`, `plus`, `pro`, `premium`, `enterprise`). Added when `enableSubscription: true` on the provider. |
377
+ | `roles` | `string` | Comma-separated role list. Added by `sigmaAdminPlugin`. |
404
378
 
405
- /** Session cookie name (default: "better-auth.session_token") */
406
- sessionCookieName?: string;
379
+ ### `session` table additions
407
380
 
408
- /** Session duration in ms (default: 30 days) */
409
- sessionDuration?: number;
381
+ | Column | Type | Description |
382
+ |--------|------|-------------|
383
+ | `subscriptionTier` | `string` | Mirrors subscription tier for fast session reads |
384
+ | `roles` | `string` | Mirrors roles for fast session reads |
410
385
 
411
- /** Custom user creation handler */
412
- createUser?: (payload, sigmaUser) => Promise<{ id: string | number }>;
386
+ ### `oauthClient` table additions (provider only)
413
387
 
414
- /** Custom user lookup handler */
415
- findUser?: (payload, sigmaUser) => Promise<{ id: string | number } | null>;
416
- }
388
+ | Column | Type | Description |
389
+ |--------|------|-------------|
390
+ | `ownerBapId` | `string` (required) | BAP ID of the client owner |
391
+ | `memberPubkey` | `string` | Public key used to verify `X-Auth-Token` on token exchange |
392
+
393
+ ---
394
+
395
+ ## Next.js integration
396
+
397
+ ### Simple callback (tokens only)
398
+
399
+ Use `createCallbackHandler` when you manage authentication state yourself (Mode A).
400
+
401
+ ```typescript
402
+ // app/api/auth/sigma/callback/route.ts
403
+ import { createCallbackHandler } from "@sigma-auth/better-auth-plugin/next";
404
+
405
+ export const runtime = "nodejs";
406
+ export const POST = createCallbackHandler({
407
+ issuerUrl: process.env.NEXT_PUBLIC_SIGMA_AUTH_URL,
408
+ clientId: process.env.NEXT_PUBLIC_SIGMA_CLIENT_ID,
409
+ callbackPath: "/auth/sigma/callback",
410
+ });
417
411
  ```
418
412
 
419
- ### Response
413
+ ### Better Auth callback (tokens + session cookie)
414
+
415
+ Use `createBetterAuthCallbackHandler` to get a session cookie set alongside the tokens. This integrates with your local Better Auth instance.
416
+
417
+ ```typescript
418
+ // app/api/auth/sigma/callback/route.ts
419
+ import { createBetterAuthCallbackHandler } from "@sigma-auth/better-auth-plugin/next";
420
+ import { auth } from "@/lib/auth-server";
421
+
422
+ export const runtime = "nodejs";
423
+ export const POST = createBetterAuthCallbackHandler({ auth });
424
+ ```
420
425
 
421
- The callback returns `PayloadCallbackResult`:
426
+ With a custom user creation handler:
422
427
 
423
428
  ```typescript
424
- {
425
- user: SigmaUserInfo; // Sigma identity data
426
- access_token: string; // Access token
427
- id_token: string; // OIDC ID token
428
- refresh_token?: string; // Refresh token (if issued)
429
- payloadUserId: string; // Local Payload user ID
430
- isNewUser: boolean; // True if user was just created
429
+ export const POST = createBetterAuthCallbackHandler({
430
+ auth,
431
+ createUser: async (adapter, sigmaUser) => {
432
+ return adapter.create({
433
+ model: "user",
434
+ data: {
435
+ email: sigmaUser.email,
436
+ name: sigmaUser.name,
437
+ emailVerified: true,
438
+ bapId: sigmaUser.bap_id,
439
+ role: "subscriber",
440
+ createdAt: new Date(),
441
+ updatedAt: new Date(),
442
+ },
443
+ });
444
+ },
445
+ });
446
+ ```
447
+
448
+ ### Error page utility
449
+
450
+ ```typescript
451
+ // app/auth/sigma/error/page.tsx
452
+ "use client";
453
+
454
+ import { parseErrorParams } from "@sigma-auth/better-auth-plugin/next";
455
+ import { useSearchParams } from "next/navigation";
456
+
457
+ export default function ErrorPage() {
458
+ const searchParams = useSearchParams();
459
+ const error = parseErrorParams(searchParams);
460
+
461
+ return (
462
+ <div>
463
+ <h1>{error?.error ?? "Authentication failed"}</h1>
464
+ <p>{error?.errorDescription}</p>
465
+ </div>
466
+ );
431
467
  }
432
468
  ```
433
469
 
434
- ## Convex Integration
470
+ ---
435
471
 
436
- For apps using Better Auth with Convex (`@convex-dev/better-auth`), use the server-side plugin instead of the Next.js callback handler. The plugin runs inside Better Auth itself — no local Auth instance needed.
472
+ ## Convex integration
437
473
 
438
- ### Setup
474
+ For apps using `@convex-dev/better-auth`, use `sigmaCallbackPlugin` inside your Convex auth configuration. No separate callback route is needed — the plugin registers `POST /sigma/callback` inside Better Auth, and the existing catch-all proxy forwards it to Convex.
439
475
 
440
476
  ```typescript
441
477
  // convex/betterAuth.ts
@@ -449,136 +485,260 @@ import authConfig from "./auth.config";
449
485
 
450
486
  export const authComponent = createClient<DataModel>(components.betterAuth);
451
487
 
452
- export const createAuth = (ctx: GenericCtx<DataModel>) => betterAuth({
453
- baseURL: process.env.SITE_URL,
454
- secret: process.env.BETTER_AUTH_SECRET,
455
- database: authComponent.adapter(ctx),
456
- plugins: [
457
- convex({ authConfig }),
458
- sigmaCallbackPlugin(), // reads env: SIGMA_MEMBER_PRIVATE_KEY, NEXT_PUBLIC_SIGMA_CLIENT_ID
459
- ],
460
- });
488
+ export const createAuth = (ctx: GenericCtx<DataModel>) =>
489
+ betterAuth({
490
+ baseURL: process.env.SITE_URL,
491
+ secret: process.env.BETTER_AUTH_SECRET,
492
+ database: authComponent.adapter(ctx),
493
+ plugins: [
494
+ convex({ authConfig }),
495
+ sigmaCallbackPlugin(),
496
+ ],
497
+ });
461
498
  ```
462
499
 
463
500
  Set environment variables in your Convex deployment:
464
501
 
465
502
  ```bash
466
- SITE_URL="https://your-site-url"
467
- BETTER_AUTH_SECRET="your-random-secret"
468
503
  bunx convex env set SIGMA_MEMBER_PRIVATE_KEY "<your-wif-key>"
469
504
  bunx convex env set NEXT_PUBLIC_SIGMA_CLIENT_ID "your-app-id"
470
505
  ```
471
506
 
472
- Set these in your Next.js app `.env.local`:
507
+ Delete `app/api/auth/sigma/callback/route.ts` if you previously had one — the catch-all proxy handles it.
473
508
 
474
- ```bash
475
- NEXT_PUBLIC_CONVEX_URL="https://your-deployment.convex.cloud"
476
- NEXT_PUBLIC_CONVEX_SITE_URL="https://your-site-url"
477
- ```
509
+ ---
478
510
 
479
- In your Next.js app, proxy `/api/auth/*` to Convex:
511
+ ## Payload CMS integration
480
512
 
481
513
  ```typescript
482
- // lib/auth-server.ts
483
- import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";
514
+ // app/api/auth/sigma/callback/route.ts
515
+ import configPromise from "@payload-config";
516
+ import { createPayloadCallbackHandler } from "@sigma-auth/better-auth-plugin/payload";
484
517
 
485
- export const { handler } = convexBetterAuthNextJs({
486
- convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
487
- convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
518
+ export const runtime = "nodejs";
519
+ export const POST = createPayloadCallbackHandler({ configPromise });
520
+ ```
521
+
522
+ With custom user creation:
523
+
524
+ ```typescript
525
+ export const POST = createPayloadCallbackHandler({
526
+ configPromise,
527
+ createUser: async (payload, sigmaUser) => {
528
+ return payload.create({
529
+ collection: "users",
530
+ data: {
531
+ email: sigmaUser.email || `${sigmaUser.sub}@sigma.identity`,
532
+ name: sigmaUser.name,
533
+ emailVerified: true,
534
+ bapId: sigmaUser.bap_id,
535
+ role: ["subscriber"],
536
+ },
537
+ });
538
+ },
488
539
  });
489
540
  ```
490
541
 
542
+ ### Payload callback options
543
+
491
544
  ```typescript
492
- // app/api/auth/[...all]/route.ts
493
- import { handler } from "@/lib/auth-server";
545
+ interface PayloadCallbackConfig {
546
+ configPromise: Promise<unknown>; // required
547
+ issuerUrl?: string; // Default: NEXT_PUBLIC_SIGMA_AUTH_URL
548
+ clientId?: string; // Default: NEXT_PUBLIC_SIGMA_CLIENT_ID
549
+ memberPrivateKey?: string; // Default: SIGMA_MEMBER_PRIVATE_KEY
550
+ callbackPath?: string; // Default: "/auth/sigma/callback"
551
+ usersCollection?: string; // Default: "users"
552
+ sessionsCollection?: string; // Default: "sessions"
553
+ sessionCookieName?: string; // Default: "better-auth.session_token"
554
+ sessionDuration?: number; // Default: 30 days (ms)
555
+ createUser?: (payload, sigmaUser) => Promise<{ id: string | number }>;
556
+ findUser?: (payload, sigmaUser) => Promise<{ id: string | number } | null>;
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Admin plugin
563
+
564
+ `sigmaAdminPlugin` resolves roles dynamically at session creation based on on-chain state. It works alongside Better Auth's built-in `admin` plugin — static roles set via `admin.setRole()` are preserved alongside dynamic ones.
565
+
566
+ ```typescript
567
+ import { betterAuth } from "better-auth";
568
+ import { admin } from "better-auth/plugins";
569
+ import { sigmaAdminPlugin } from "@sigma-auth/better-auth-plugin/server";
570
+
571
+ export const auth = betterAuth({
572
+ plugins: [
573
+ sigmaAdminPlugin({
574
+ // Grant a role to holders of a specific NFT collection
575
+ nftCollections: [
576
+ { id: "abc123_0", role: "pixel-fox-holder" },
577
+ { id: "def456_0", role: "premium" },
578
+ ],
579
+
580
+ // Grant a role based on minimum token balance
581
+ tokenGates: [
582
+ { ticker: "GM", threshold: 75000, role: "premium" },
583
+ { ticker: "GM", threshold: 1000000, role: "whale" },
584
+ ],
585
+
586
+ // Grant admin role to specific BAP identities
587
+ adminBAPIds: [process.env.SUPERADMIN_BAP_ID!],
588
+
589
+ // Fetch connected wallets for NFT/token checking
590
+ getWalletAddresses: async (userId) => {
591
+ return db.query("SELECT address FROM wallets WHERE user_id = $1", [userId]);
592
+ },
593
+
594
+ // NFT ownership check (implement with your indexer)
595
+ checkNFTOwnership: async (address, collectionId) => {
596
+ const nfts = await fetchNftUtxos(address, collectionId);
597
+ return nfts.length > 0;
598
+ },
599
+
600
+ // Token balance check (implement with your indexer)
601
+ getTokenBalance: async (address, ticker) => {
602
+ return fetchTokenBalance(address, ticker);
603
+ },
494
604
 
495
- export const { GET, POST } = handler;
605
+ // BAP profile resolver
606
+ getBAPProfile: async (userId) => {
607
+ return db.profiles.findByUserId(userId);
608
+ },
609
+
610
+ // Custom role resolution logic
611
+ extendRoles: async (user, bap, address) => {
612
+ const roles: string[] = [];
613
+ if (await isVerifiedCreator(bap?.idKey)) {
614
+ roles.push("creator");
615
+ }
616
+ return roles;
617
+ },
618
+ }),
619
+
620
+ admin({ defaultRole: "user" }),
621
+ ],
622
+ });
496
623
  ```
497
624
 
498
- The plugin registers `POST /sigma/callback` the existing `sigmaClient()` client plugin works unchanged. The catch-all auth proxy (`app/api/auth/[...all]/route.ts`) forwards the request to Convex where the plugin handles it.
625
+ Roles are attached to both `session.user.roles` and `user.roles` as a comma-separated string, updated on every session creation and session fetch.
499
626
 
500
- No separate callback route needed — delete `app/api/auth/sigma/callback/route.ts` if you have one.
627
+ ---
501
628
 
502
- ## Server Plugin (Auth Provider)
629
+ ## Provider plugin (running your own auth server)
503
630
 
504
- For building your own Sigma Identity server:
631
+ If you are building a Sigma-compatible auth server (like `auth.sigmaidentity.com` itself), use `sigmaProvider` to add Bitcoin signature verification to the OIDC token endpoint and BAP identity support.
505
632
 
506
633
  ```typescript
507
634
  import { betterAuth } from "better-auth";
508
- import { sigmaProvider } from "@sigma-auth/better-auth-plugin/provider";
635
+ import { sigmaProvider, createBapOrganization } from "@sigma-auth/better-auth-plugin/provider";
509
636
 
510
637
  export const auth = betterAuth({
511
638
  plugins: [
512
639
  sigmaProvider({
513
- enableSubscription: true,
640
+ // Resolve Bitcoin pubkey to BAP identity
514
641
  resolveBAPId: async (pool, userId, pubkey, register) => {
515
- // Your BAP ID resolution logic
642
+ const result = await pool.query(
643
+ "SELECT bap_id FROM profile WHERE member_pubkey = $1",
644
+ [pubkey],
645
+ );
646
+ return result.rows[0]?.bap_id ?? null;
516
647
  },
517
- getPool: () => database,
518
- cache: redisCache,
648
+
649
+ getPool: () => databasePool,
650
+
651
+ cache: redisClient,
652
+
653
+ enableSubscription: true,
654
+
655
+ debug: process.env.NODE_ENV === "development",
519
656
  }),
657
+
658
+ // BAP identities map to organizations (one org per identity)
659
+ createBapOrganization(),
520
660
  ],
521
661
  });
522
662
  ```
523
663
 
524
- ## Key Concepts
664
+ The provider plugin intercepts `POST /oauth2/token` to validate the Bitcoin-signed `X-Auth-Token` header — verifying the signature pubkey matches the `memberPubkey` registered for the OAuth client — and to update the user's name and avatar from their selected BAP profile once the token exchange succeeds.
525
665
 
526
- ### OAuth Endpoints
666
+ ---
527
667
 
528
- When using OAuth flow, there are **two different endpoints**:
668
+ ## Type reference
529
669
 
530
- 1. **OAuth Redirect URI** (`/auth/sigma/callback`) - Where the auth server redirects after authorization
531
- 2. **Token Exchange API** (`/api/auth/sigma/callback`) - Internal endpoint that exchanges code for tokens
670
+ ### `SigmaUserInfo`
532
671
 
533
- The redirect URI is what you configure in your OAuth client settings. The token exchange API is called internally by your callback page.
672
+ Returned by `handleCallback` and the `userinfo` endpoint.
534
673
 
535
- ### Authentication Result
674
+ ```typescript
675
+ interface SigmaUserInfo {
676
+ sub: string; // User ID
677
+ name?: string;
678
+ given_name?: string;
679
+ family_name?: string;
680
+ picture?: string;
681
+ email?: string;
682
+ pubkey: string; // Bitcoin public key
683
+ bap_id?: string; // BAP identity ID
684
+ bap?: BAPProfile; // Full BAP profile
685
+ }
686
+ ```
536
687
 
537
- After successful authentication via `handleCallback()`, you receive:
688
+ ### `BAPProfile`
538
689
 
539
690
  ```typescript
540
- {
541
- user: {
542
- sub: string; // User ID
543
- name?: string; // Display name
544
- email?: string; // Email (if available)
545
- picture?: string; // Avatar URL
546
- pubkey: string; // Bitcoin public key
547
- bap?: { // BAP identity (if available)
548
- idKey: string; // BAP ID
549
- identity: {
550
- name?: string;
551
- alternateName?: string;
552
- description?: string;
553
- // ... other BAP profile fields
554
- };
555
- };
691
+ interface BAPProfile {
692
+ idKey: string; // BAP identity key
693
+ rootAddress: string; // Root Bitcoin address
694
+ currentAddress?: string;
695
+ identity?: {
696
+ "@type"?: string;
697
+ alternateName?: string; // Display name
698
+ givenName?: string;
699
+ familyName?: string;
700
+ image?: string;
701
+ banner?: string;
702
+ description?: string;
556
703
  };
557
- access_token: string; // Access token for API calls
558
- id_token: string; // JWT ID token (OIDC)
559
- refresh_token?: string; // Refresh token (if issued)
560
704
  }
561
705
  ```
562
706
 
563
- ## Features
707
+ ### `OAuthCallbackResult`
708
+
709
+ ```typescript
710
+ interface OAuthCallbackResult {
711
+ user: SigmaUserInfo;
712
+ access_token: string;
713
+ id_token: string; // OIDC JWT
714
+ refresh_token?: string;
715
+ }
716
+ ```
564
717
 
565
- - PKCE flow for public clients
566
- - Bitcoin Auth signatures for secure token exchange
567
- - BAP (Bitcoin Attestation Protocol) identity support
568
- - Multi-identity wallet support
569
- - Subscription tier verification via NFT ownership
570
- - Type-safe with full TypeScript support
571
- - Full OIDC compliance with ID tokens
718
+ ### `SubscriptionStatus`
719
+
720
+ ```typescript
721
+ interface SubscriptionStatus {
722
+ tier: "free" | "plus" | "pro" | "premium" | "enterprise";
723
+ isActive: boolean;
724
+ nftOrigin?: string;
725
+ walletAddress?: string;
726
+ expiresAt?: Date;
727
+ features?: string[];
728
+ }
729
+ ```
730
+
731
+ ---
572
732
 
573
733
  ## Troubleshooting
574
734
 
575
- ### 403 on Token Exchange (Most Common Issue)
735
+ ### 403 on token exchange
576
736
 
577
- **Symptom**: OAuth flow succeeds, user authenticates, redirects back with `?code=...&state=...`, but then you get "Token Exchange Failed - Server returned 403".
737
+ **Symptom:** OAuth completes, redirect comes back with `?code=...`, but then you get "Token Exchange Failed Server returned 403."
578
738
 
579
- **Cause**: Better Auth's CSRF protection rejects the POST because the requesting origin isn't in `trustedOrigins`. This is extremely common on Vercel preview deployments where URLs are dynamic.
739
+ **Cause:** Better Auth's CSRF protection rejects the POST because the requesting origin is not in `trustedOrigins`. This is common on Vercel preview deployments where URLs change per branch.
580
740
 
581
- **Fix**: Add Vercel's auto-set environment variables to your `trustedOrigins`:
741
+ **Fix:** Add Vercel's automatic environment variables to `trustedOrigins`:
582
742
 
583
743
  ```typescript
584
744
  export const auth = betterAuth({
@@ -588,27 +748,74 @@ export const auth = betterAuth({
588
748
  process.env.VERCEL_BRANCH_URL ? `https://${process.env.VERCEL_BRANCH_URL}` : "",
589
749
  "http://localhost:3000",
590
750
  ].filter(Boolean),
591
- // ...
592
751
  });
593
752
  ```
594
753
 
595
- This is a Better Auth configuration issue, not a Sigma plugin issue. See the [setup-nextjs skill](#available-skills) for more troubleshooting tips.
754
+ This is a Better Auth configuration requirement.
755
+
756
+ ### Callback URL not registered
757
+
758
+ Register every domain you deploy to — including Vercel preview URLs — as an allowed redirect URI in your Sigma OAuth client settings.
759
+
760
+ ### `SIGMA_MEMBER_PRIVATE_KEY` mismatch
596
761
 
597
- ### Callback URL Not Registered
762
+ The WIF key in your environment must correspond to the public key registered as `memberPubkey` on your OAuth client record. If they differ, the signature verification step will reject every token exchange with `invalid_client`.
598
763
 
599
- Ensure your callback URL is registered in your Sigma client configuration for every domain you deploy to, including Vercel preview URLs.
764
+ To verify: derive the public key from your WIF using `@bsv/sdk`, then compare it to the `memberPubkey` in your database.
600
765
 
601
- ### Environment Variable Issues
766
+ ### `Missing id_token in token response`
602
767
 
603
- Run the validation script to check all required env vars:
768
+ Ensure `scope: "openid profile"` is included in the authorization request. The `openid` scope is required for OIDC `id_token` issuance.
769
+
770
+ ### Local signer not detected
771
+
772
+ The local [TokenPass](https://tokenpas.app) server must be running at `http://localhost:21000` (or your configured `localServerUrl`) before the page loads. The probe runs once on initialization; if the server starts after the page loads, call `authClient.sigma.detectServer()` to retry.
773
+
774
+ ---
775
+
776
+ ## Claude Code plugin
777
+
778
+ An AI-assisted setup plugin is available for Claude Code:
604
779
 
605
780
  ```bash
606
- bun run skills/setup-nextjs/scripts/validate-env.ts
781
+ claude plugin install sigma-auth@b-open-io
607
782
  ```
608
783
 
609
- ## Documentation
784
+ Available skills:
785
+
786
+ | Skill | Command | Description |
787
+ |-------|---------|-------------|
788
+ | setup-nextjs | `/sigma-auth:setup-nextjs` | Project detection, env validation, and health check |
789
+ | setup-convex | `/sigma-auth:setup-convex` | Convex + Better Auth integration guide |
790
+ | bitcoin-auth-diagnostics | `/sigma-auth:bitcoin-auth-diagnostics` | Diagnose token verification and signature errors |
791
+ | tokenpass | `/sigma-auth:tokenpass` | Token-gated access patterns with NFT ownership |
792
+ | device-authorization | `/sigma-auth:device-authorization` | Device authorization flow for CLI tools and IoT |
793
+
794
+ ---
795
+
796
+ ## Why Bitcoin authentication
797
+
798
+ | Concern | Password auth | Bitcoin auth |
799
+ |---------|--------------|--------------|
800
+ | Password breach | All users affected | Irrelevant — authentication uses cryptographic signatures |
801
+ | Account recovery | Email link or support ticket | Encrypted backup, recoverable by the user |
802
+ | Identity portability | Locked to one provider | Same identity works across any Sigma-compatible app |
803
+ | Phishing resistance | Vulnerable to credential theft | Signatures are request-specific and non-replayable |
804
+ | Privacy | Provider knows your email | Only your pubkey is required |
805
+ | Regulatory exposure | PII storage obligations | No PII unless the user voluntarily provides it |
806
+
807
+ The Sigma Identity flow is a single redirect with the same UX as "Sign in with Google" — the difference is that the resulting identity belongs to the user and works across every app that accepts Bitcoin signatures.
808
+
809
+ ---
810
+
811
+ ## Resources
610
812
 
611
- Full documentation: [https://sigmaidentity.com/docs](https://sigmaidentity.com/docs)
813
+ - **Sigma Identity** — [sigmaidentity.com](https://sigmaidentity.com)
814
+ - **Full documentation** — [sigmaidentity.com/docs](https://sigmaidentity.com/docs)
815
+ - **Better Auth** — [better-auth.com](https://www.better-auth.com)
816
+ - **BAP specification** — [bap.network](https://bap.network)
817
+ - **GitHub** — [github.com/b-open-io/better-auth-plugin](https://github.com/b-open-io/better-auth-plugin)
818
+ - **Issues** — [github.com/b-open-io/better-auth-plugin/issues](https://github.com/b-open-io/better-auth-plugin/issues)
612
819
 
613
820
  ## License
614
821