@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 +574 -367
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -7
- package/dist/client/index.js.map +1 -1
- package/dist/client/sigma-cwi.d.ts +45 -0
- package/dist/client/sigma-cwi.d.ts.map +1 -0
- package/dist/client/sigma-cwi.js +257 -0
- package/dist/client/sigma-cwi.js.map +1 -0
- package/dist/server/admin.d.ts.map +1 -1
- package/dist/server/admin.js +2 -1
- package/dist/server/admin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,150 +1,70 @@
|
|
|
1
1
|
# @sigma-auth/better-auth-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
## Features
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
+
## Installation
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
|
|
29
|
+
bun add @sigma-auth/better-auth-plugin
|
|
30
|
+
# or
|
|
31
|
+
npm install @sigma-auth/better-auth-plugin
|
|
27
32
|
```
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
### Peer dependencies
|
|
30
35
|
|
|
31
|
-
|
|
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
|
-
#
|
|
43
|
-
bun
|
|
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
|
-
|
|
42
|
+
# Required for server-side token exchange
|
|
43
|
+
bun add bitcoin-auth
|
|
111
44
|
|
|
112
|
-
|
|
45
|
+
# Required for the provider plugin (running your own auth server)
|
|
46
|
+
bun add @bsv/sdk bsv-bap @neondatabase/serverless zod
|
|
113
47
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
52
|
+
## Quick Start
|
|
131
53
|
|
|
132
|
-
This is the standard setup for
|
|
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.
|
|
56
|
+
### 1. Set environment variables
|
|
135
57
|
|
|
136
58
|
```bash
|
|
137
|
-
#
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
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
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
handleCallback();
|
|
111
|
+
})
|
|
112
|
+
.catch((err) => {
|
|
113
|
+
authClient.sigma.redirectToError(err);
|
|
114
|
+
});
|
|
216
115
|
}, [searchParams, router]);
|
|
217
116
|
|
|
218
|
-
|
|
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={<
|
|
122
|
+
<Suspense fallback={<p>Loading...</p>}>
|
|
248
123
|
<CallbackContent />
|
|
249
124
|
</Suspense>
|
|
250
125
|
);
|
|
251
126
|
}
|
|
252
127
|
```
|
|
253
128
|
|
|
254
|
-
### 5.
|
|
129
|
+
### 5. Add a sign-in button
|
|
255
130
|
|
|
256
131
|
```typescript
|
|
257
|
-
|
|
258
|
-
import { signIn } from "@/lib/auth";
|
|
132
|
+
import { authClient } from "@/lib/auth";
|
|
259
133
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
---
|
|
271
152
|
|
|
272
|
-
|
|
273
|
-
// Example with Context
|
|
274
|
-
import { createContext, useContext, useEffect, useState } from "react";
|
|
153
|
+
## Architecture
|
|
275
154
|
|
|
276
|
-
|
|
155
|
+
### How Bitcoin authentication works
|
|
277
156
|
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
const storedUser = localStorage.getItem("sigma_user");
|
|
283
|
-
if (storedUser) {
|
|
284
|
-
setUser(JSON.parse(storedUser));
|
|
285
|
-
}
|
|
286
|
-
}, []);
|
|
159
|
+
The flow:
|
|
287
160
|
|
|
288
|
-
|
|
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
|
-
|
|
170
|
+
### Signer architecture
|
|
292
171
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
## Server configuration (Mode B)
|
|
301
203
|
|
|
302
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
332
|
-
|
|
261
|
+
// Redirect to error page with structured error params
|
|
262
|
+
authClient.sigma.redirectToError(caughtError);
|
|
333
263
|
```
|
|
334
264
|
|
|
335
|
-
|
|
265
|
+
### Identity management
|
|
336
266
|
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
###
|
|
315
|
+
### Signer detection
|
|
358
316
|
|
|
359
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
###
|
|
354
|
+
### Subscriptions
|
|
381
355
|
|
|
382
356
|
```typescript
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
361
|
+
// Check if current tier meets a minimum requirement
|
|
362
|
+
const hasAccess = authClient.subscription.hasTier(status.tier, "pro");
|
|
363
|
+
```
|
|
389
364
|
|
|
390
|
-
|
|
391
|
-
clientId?: string;
|
|
365
|
+
---
|
|
392
366
|
|
|
393
|
-
|
|
394
|
-
memberPrivateKey?: string;
|
|
367
|
+
## Database schema
|
|
395
368
|
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
usersCollection?: string;
|
|
371
|
+
### `user` table additions
|
|
401
372
|
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
sessionCookieName?: string;
|
|
379
|
+
### `session` table additions
|
|
407
380
|
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
412
|
-
createUser?: (payload, sigmaUser) => Promise<{ id: string | number }>;
|
|
386
|
+
### `oauthClient` table additions (provider only)
|
|
413
387
|
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
426
|
+
With a custom user creation handler:
|
|
422
427
|
|
|
423
428
|
```typescript
|
|
424
|
-
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
470
|
+
---
|
|
435
471
|
|
|
436
|
-
|
|
472
|
+
## Convex integration
|
|
437
473
|
|
|
438
|
-
|
|
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>) =>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
507
|
+
Delete `app/api/auth/sigma/callback/route.ts` if you previously had one — the catch-all proxy handles it.
|
|
473
508
|
|
|
474
|
-
|
|
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
|
-
|
|
511
|
+
## Payload CMS integration
|
|
480
512
|
|
|
481
513
|
```typescript
|
|
482
|
-
//
|
|
483
|
-
import
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
627
|
+
---
|
|
501
628
|
|
|
502
|
-
##
|
|
629
|
+
## Provider plugin (running your own auth server)
|
|
503
630
|
|
|
504
|
-
|
|
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
|
-
|
|
640
|
+
// Resolve Bitcoin pubkey to BAP identity
|
|
514
641
|
resolveBAPId: async (pool, userId, pubkey, register) => {
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
+
---
|
|
527
667
|
|
|
528
|
-
|
|
668
|
+
## Type reference
|
|
529
669
|
|
|
530
|
-
|
|
531
|
-
2. **Token Exchange API** (`/api/auth/sigma/callback`) - Internal endpoint that exchanges code for tokens
|
|
670
|
+
### `SigmaUserInfo`
|
|
532
671
|
|
|
533
|
-
|
|
672
|
+
Returned by `handleCallback` and the `userinfo` endpoint.
|
|
534
673
|
|
|
535
|
-
|
|
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
|
-
|
|
688
|
+
### `BAPProfile`
|
|
538
689
|
|
|
539
690
|
```typescript
|
|
540
|
-
{
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
735
|
+
### 403 on token exchange
|
|
576
736
|
|
|
577
|
-
**Symptom
|
|
737
|
+
**Symptom:** OAuth completes, redirect comes back with `?code=...`, but then you get "Token Exchange Failed — Server returned 403."
|
|
578
738
|
|
|
579
|
-
**Cause
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
766
|
+
### `Missing id_token in token response`
|
|
602
767
|
|
|
603
|
-
|
|
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
|
-
|
|
781
|
+
claude plugin install sigma-auth@b-open-io
|
|
607
782
|
```
|
|
608
783
|
|
|
609
|
-
|
|
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
|
-
|
|
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
|
|