@rhinestone/1auth 0.7.0 → 0.7.1
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 +148 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,11 @@ The SDK provides multiple entry points for different use cases:
|
|
|
26
26
|
|
|
27
27
|
| Entry Point | Import | Use Case |
|
|
28
28
|
|-------------|--------|----------|
|
|
29
|
-
| Default | `@rhinestone/1auth` | Core client, provider, types |
|
|
30
|
-
| React | `@rhinestone/1auth/react` | PayButton component |
|
|
31
|
-
| Server | `@rhinestone/1auth/server` | JWT
|
|
29
|
+
| Default | `@rhinestone/1auth` | Core client, provider, batch components, registry, types |
|
|
30
|
+
| React | `@rhinestone/1auth/react` | `PayButton` component + `useSponsorship` hook |
|
|
31
|
+
| Server | `@rhinestone/1auth/server` | JWT sponsorship signers for app-sponsored intents |
|
|
32
32
|
| Wagmi | `@rhinestone/1auth/wagmi` | Wagmi connector integration |
|
|
33
|
+
| Headless | `@rhinestone/1auth/headless` | Headless client without dialog UI |
|
|
33
34
|
|
|
34
35
|
## Quick Start
|
|
35
36
|
|
|
@@ -40,25 +41,34 @@ import { OneAuthClient, createOneAuthProvider } from '@rhinestone/1auth';
|
|
|
40
41
|
|
|
41
42
|
// Create the client
|
|
42
43
|
const client = new OneAuthClient({
|
|
43
|
-
|
|
44
|
+
providerUrl: 'https://passkey.1auth.box',
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
// Create an EIP-1193 compatible provider
|
|
47
|
-
const provider = createOneAuthProvider(client);
|
|
47
|
+
// Create an EIP-1193 compatible provider (chainId is required)
|
|
48
|
+
const provider = createOneAuthProvider({ client, chainId: 8453 }); // Base
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
### React Integration
|
|
51
52
|
|
|
52
53
|
```tsx
|
|
53
54
|
import { PayButton } from '@rhinestone/1auth/react';
|
|
55
|
+
import { OneAuthClient } from '@rhinestone/1auth';
|
|
56
|
+
|
|
57
|
+
const client = new OneAuthClient({ providerUrl: 'https://passkey.1auth.box' });
|
|
54
58
|
|
|
55
59
|
function CheckoutPage() {
|
|
56
60
|
return (
|
|
57
61
|
<PayButton
|
|
58
|
-
|
|
62
|
+
client={client}
|
|
63
|
+
intent={{
|
|
64
|
+
targetChain: 8453, // Base
|
|
65
|
+
calls: [{ to: '0xRecipient', data: '0x', value: '1000000' }],
|
|
66
|
+
}}
|
|
59
67
|
onSuccess={(result) => console.log('Payment successful', result)}
|
|
60
68
|
onError={(error) => console.error('Payment failed', error)}
|
|
61
|
-
|
|
69
|
+
>
|
|
70
|
+
Pay $5 USDC
|
|
71
|
+
</PayButton>
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
```
|
|
@@ -67,15 +77,17 @@ function CheckoutPage() {
|
|
|
67
77
|
|
|
68
78
|
```typescript
|
|
69
79
|
import { oneAuth } from '@rhinestone/1auth/wagmi';
|
|
80
|
+
import { OneAuthClient } from '@rhinestone/1auth';
|
|
70
81
|
import { createConfig, http } from 'wagmi';
|
|
71
82
|
import { mainnet, sepolia } from 'wagmi/chains';
|
|
72
83
|
|
|
84
|
+
// The connector wraps a configured OneAuthClient.
|
|
85
|
+
const client = new OneAuthClient({ providerUrl: 'https://passkey.1auth.box' });
|
|
86
|
+
|
|
73
87
|
const config = createConfig({
|
|
74
88
|
chains: [mainnet, sepolia],
|
|
75
89
|
connectors: [
|
|
76
|
-
oneAuth({
|
|
77
|
-
passkeyServerUrl: 'https://passkey.1auth.box',
|
|
78
|
-
}),
|
|
90
|
+
oneAuth({ client }),
|
|
79
91
|
],
|
|
80
92
|
transports: {
|
|
81
93
|
[mainnet.id]: http(),
|
|
@@ -87,19 +99,15 @@ const config = createConfig({
|
|
|
87
99
|
### Server-Side JWT Sponsorship
|
|
88
100
|
|
|
89
101
|
```typescript
|
|
90
|
-
import {
|
|
102
|
+
import { createSponsorshipSigner } from '@rhinestone/1auth/server';
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
projectId: process.env.RHINESTONE_PROJECT_ID,
|
|
96
|
-
appId: process.env.RHINESTONE_APP_ID,
|
|
97
|
-
keyId: process.env.RHINESTONE_KEY_ID,
|
|
98
|
-
});
|
|
104
|
+
// Reads RHINESTONE_JWT_PRIVATE_KEY / _INTEGRATOR_ID / _PROJECT_ID / _APP_ID / _KEY_ID
|
|
105
|
+
// from process.env. Pass `{ credentials }` to supply them explicitly instead.
|
|
106
|
+
const signer = createSponsorshipSigner();
|
|
99
107
|
|
|
100
108
|
// Use in your API routes
|
|
101
109
|
const accessToken = await signer.accessToken();
|
|
102
|
-
const extensionToken = await signer.
|
|
110
|
+
const extensionToken = await signer.extensionToken(intentOp); // accepts the JSON-stringified intentOp
|
|
103
111
|
```
|
|
104
112
|
|
|
105
113
|
## Core Exports
|
|
@@ -108,22 +116,27 @@ const extensionToken = await signer.getIntentExtensionToken(intentInput);
|
|
|
108
116
|
|
|
109
117
|
- `OneAuthClient` - Main client for passkey operations
|
|
110
118
|
- `createOneAuthProvider()` - EIP-1193 compatible provider factory
|
|
111
|
-
- `createPasskeyWalletClient()` - viem WalletClient with passkey support
|
|
112
|
-
- `
|
|
113
|
-
- `
|
|
114
|
-
-
|
|
119
|
+
- `createPasskeyAccount()`, `createPasskeyWalletClient()` - viem account / WalletClient with passkey support
|
|
120
|
+
- `definePermissions()`, `createCrossChainPermission()` - Session-key permission helpers
|
|
121
|
+
- `BatchQueueProvider`, `useBatchQueue()`, `BatchQueueWidget` - Batch queue context, hook, and widget
|
|
122
|
+
- `getSupportedChains()`, `getSupportedTokens()`, `getAllSupportedChainsAndTokens()` - Chain/token registry
|
|
123
|
+
- `hashMessage()`, `verifyMessageHash()` - Message verification
|
|
124
|
+
- Type exports for intents, accounts, sponsorship, and more
|
|
115
125
|
|
|
116
126
|
### `@rhinestone/1auth/react`
|
|
117
127
|
|
|
118
|
-
- `PayButton` - Pre-built payment/intent button component
|
|
119
|
-
- `
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
- `PayButton` - Pre-built payment/intent button component (requires a `client` prop)
|
|
129
|
+
- `useSponsorship()` - Hook that builds a sponsorship config from token endpoint URLs with an expiry-aware access-token cache
|
|
130
|
+
|
|
131
|
+
> Batch components (`BatchQueueProvider`, `BatchQueueWidget`, `useBatchQueue`) are exported from the **main** `@rhinestone/1auth` entry, not from `/react`.
|
|
122
132
|
|
|
123
133
|
### `@rhinestone/1auth/server`
|
|
124
134
|
|
|
125
|
-
- `
|
|
126
|
-
- `
|
|
135
|
+
- `createSponsorshipSigner()` - **Recommended.** Framework-agnostic signer that reads credentials from env and exposes `accessToken()` / `extensionToken(intentOp)`
|
|
136
|
+
- `createJwtSigner()` - Lower-level escape hatch for custom claims; takes `{ jwt, shouldSponsor? }`
|
|
137
|
+
- `verifyOneAuthAccount()` - Server-side check that an address belongs to a known 1auth account
|
|
138
|
+
- `hashMessage()`, `verifyMessageHash()`, `encodeWebAuthnSignature()` - Verification / signing utilities
|
|
139
|
+
- `JwtSignerConfig`, `JwtCredentials`, `SponsorshipSigner` - Configuration types
|
|
127
140
|
|
|
128
141
|
### `@rhinestone/1auth/wagmi`
|
|
129
142
|
|
|
@@ -131,7 +144,19 @@ const extensionToken = await signer.getIntentExtensionToken(intentInput);
|
|
|
131
144
|
|
|
132
145
|
## App-Sponsored Intents
|
|
133
146
|
|
|
134
|
-
|
|
147
|
+
### What the JWT is for
|
|
148
|
+
|
|
149
|
+
Every intent needs someone to pay its fees. **By default, 1auth pays** using its own orchestrator API key — you don't have to do anything. App sponsorship lets *your app* take over that bill instead: you mint a short-lived JWT, signed with a private key Rhinestone issued to your project, and the orchestrator charges your account for any intent that JWT authorizes.
|
|
150
|
+
|
|
151
|
+
The JWT is purely a **billing authorization** — it says "this app vouches for paying this intent." It does **not** authenticate the user or authorize the transaction itself; that's the passkey/WebAuthn signature, which is independent. Sponsorship and signing happen in parallel.
|
|
152
|
+
|
|
153
|
+
The private key must **never** reach the browser, so the tokens are always minted server-side. You have two ways to wire that up:
|
|
154
|
+
|
|
155
|
+
1. **Self-host the signer (full control).** Run `createSponsorshipSigner()` (or the lower-level `createJwtSigner()`) from `@rhinestone/1auth/server` behind two endpoints on your own backend, gated by your existing session auth. This is where you enforce per-user policy — spending limits, allowlists, the `shouldSponsor` filter. You decide exactly when and for whom a token is minted.
|
|
156
|
+
|
|
157
|
+
2. **Let the SDK drive it.** Point `OneAuthClient` at those two endpoint URLs (the `SponsorshipUrlConfig` form) and the SDK handles fetching, caching, expiry, and retry for you — including the `useSponsorship` React hook. You still host the endpoints (the signing key lives there), but you don't write any client-side token plumbing.
|
|
158
|
+
|
|
159
|
+
In short: the **signing** always lives on your backend; the **fetching** can either be your code (callback form) or the SDK's (URL form). Pick based on how much of the token lifecycle you want to own.
|
|
135
160
|
|
|
136
161
|
### How It Works
|
|
137
162
|
|
|
@@ -165,13 +190,103 @@ sequenceDiagram
|
|
|
165
190
|
PS-->>SDK: { intentId }
|
|
166
191
|
```
|
|
167
192
|
|
|
193
|
+
### The two tokens
|
|
194
|
+
|
|
195
|
+
App sponsorship is authorized by two short-lived JWTs your backend mints with `createSponsorshipSigner()` (or `createJwtSigner()`). Both are signed with your Rhinestone JWK — `ES256` for an EC key, `RS256` for RSA — and carry your `integratorId` (issuer), `projectId` (subject), and `appId`.
|
|
196
|
+
|
|
197
|
+
| Token | `typ` | TTL | Purpose |
|
|
198
|
+
|-------|-------|-----|---------|
|
|
199
|
+
| **Access token** | `access` | 1 hour | Identifies your app to the orchestrator. Not bound to any intent, so it's safe to cache and reuse across many intents (the SDK's `useSponsorship` hook does exactly this). |
|
|
200
|
+
| **Extension token** | `intent_extension` | 5 minutes | Authorizes sponsorship of **one specific intent**. Minted fresh per intent. |
|
|
201
|
+
|
|
202
|
+
The extension token is what makes sponsorship safe to expose to the browser: its `policy.sponsorship.intent_input.digest` claim is a **JCS-canonicalized ([RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)) SHA-256 hash of the exact `intentOp`** being signed. The orchestrator recomputes that digest from the intent it receives and rejects the token if they differ — so a token minted for "send $5 to Alice" cannot be replayed to authorize "send $5000 to Mallory". Each extension token also carries a unique `jti`.
|
|
203
|
+
|
|
204
|
+
Because the digest pins the token to one intent, your backend's `extension-token` endpoint is the natural place to enforce per-user policy (spending limits, allowlists) — see the `shouldSponsor` filter on `createJwtSigner` / `createSponsorshipSigner`, which can veto by chain, account, or calls before a token is ever minted.
|
|
205
|
+
|
|
206
|
+
#### What the tokens actually contain
|
|
207
|
+
|
|
208
|
+
If you decode the JWTs your backend mints (e.g. paste into [jwt.io](https://jwt.io)), this is the shape. Both share the same header and `iss`/`sub`/`aud` claims — only `typ`, lifetime, and the intent-binding differ.
|
|
209
|
+
|
|
210
|
+
```jsonc
|
|
211
|
+
// Header (both tokens) — alg is ES256 for an EC key, RS256 for RSA
|
|
212
|
+
{ "alg": "ES256", "kid": "<keyId>" }
|
|
213
|
+
|
|
214
|
+
// Access token payload — app identity, reusable for 1h
|
|
215
|
+
{
|
|
216
|
+
"typ": "access",
|
|
217
|
+
"app_id": "<appId>",
|
|
218
|
+
"iss": "<integratorId>", // setIssuer
|
|
219
|
+
"sub": "<projectId>", // setSubject
|
|
220
|
+
"aud": "rhinestone-api", // override via JwtCredentials.audience
|
|
221
|
+
"iat": 1719300000,
|
|
222
|
+
"exp": 1719303600 // iat + 1h
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Extension token payload — authorizes ONE intent, expires in 5m
|
|
226
|
+
{
|
|
227
|
+
"typ": "intent_extension",
|
|
228
|
+
"app_id": "<appId>",
|
|
229
|
+
"jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479", // unique per token
|
|
230
|
+
"policy": {
|
|
231
|
+
"sponsorship": {
|
|
232
|
+
"scope": "intent",
|
|
233
|
+
"intent_input": {
|
|
234
|
+
// JCS-canonicalized SHA-256 of the exact intentOp; the orchestrator
|
|
235
|
+
// recomputes this and rejects the token if it doesn't match.
|
|
236
|
+
"digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"iss": "<integratorId>",
|
|
241
|
+
"sub": "<projectId>",
|
|
242
|
+
"aud": "rhinestone-api",
|
|
243
|
+
"iat": 1719300000,
|
|
244
|
+
"exp": 1719300300 // iat + 5m
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Token lifecycle & caching
|
|
249
|
+
|
|
250
|
+
The access token is reusable, so re-minting it on every intent is wasteful — but serving a *stale* one is worse. `useSponsorship` / `createCachedSponsorship` hold the access token in a closure and only reuse it while its `exp` is comfortably in the future:
|
|
251
|
+
|
|
252
|
+
- A **60-second safety margin** (`ACCESS_TOKEN_EXPIRY_MARGIN_MS`) is subtracted from `exp` before deciding a cached token is still usable. This absorbs clock skew between your app server and the orchestrator's verifier plus the in-flight time of the `prepare` call — a token that looks valid locally can still be rejected downstream if it expires mid-request.
|
|
253
|
+
- A token that can't be decoded, or carries no numeric `exp`, is treated as unusable so a malformed cache entry never gets served indefinitely.
|
|
254
|
+
- If the **extension-token** endpoint returns `401`, the cached access token is dropped immediately, so the next call re-mints rather than retrying with a token the orchestrator just rejected.
|
|
255
|
+
- The **extension token is never cached** — it binds to a specific `intentOp`, so a fresh one is minted per intent.
|
|
256
|
+
|
|
257
|
+
Both fetches use `credentials: "include"` so your app's own session cookie authenticates the user; no separate auth channel is needed.
|
|
258
|
+
|
|
259
|
+
#### Troubleshooting
|
|
260
|
+
|
|
261
|
+
| Symptom | Likely cause | Fix |
|
|
262
|
+
|---------|--------------|-----|
|
|
263
|
+
| `"Failed to get quote from orchestrator"` (a 401 surfacing as a quote failure) | The access token's `exp` lapsed before the orchestrator verified it (`"exp" claim timestamp check failed`) — usually a long-lived tab serving a stale cached token, or large clock skew | Use `useSponsorship` / `createCachedSponsorship` (they re-mint with the 60s margin) rather than hand-rolling a cache; check server clock sync |
|
|
264
|
+
| `401` from your own `access-token` / `extension-token` endpoint | Your session guard rejected the request (user not logged in, cookie not sent cross-origin) | Ensure the endpoints are **same-origin** with your app and that the client sends credentials; gate with your existing session auth |
|
|
265
|
+
| Sponsorship silently denied / `SponsorshipDeniedError` | A `shouldSponsor` filter vetoed the intent (chain/account/calls not allowed) | Confirm the intent matches your policy, or relax the filter |
|
|
266
|
+
| Orchestrator rejects the extension token despite a valid signature | `intent_input.digest` doesn't match — the `intentOp` was mutated between `prepare` and `execute`, or you minted the token from a different object than the SDK sent | Mint the extension token from the exact `intentOp` string the SDK posts to your endpoint; don't re-serialize or reorder it |
|
|
267
|
+
| `Unsupported JWK kty` / `Unsupported EC curve` at startup | The private key JWK isn't an EC (`P-256`/`P-384`/`P-521`) or RSA key | Use the JWK Rhinestone issued; `ES256` (EC P-256) is the common case |
|
|
268
|
+
|
|
168
269
|
### Why this shape
|
|
169
270
|
|
|
170
271
|
The SDK calls your token endpoints **from the browser, same-origin**. Your app's session cookie naturally authenticates the request — no need to pass bearer tokens through the SDK. The passkey service never fetches from your backend on your behalf, so there's no SSRF surface and no CORS config to worry about.
|
|
171
272
|
|
|
172
273
|
### Setup
|
|
173
274
|
|
|
174
|
-
**1.
|
|
275
|
+
**1. Get a signing key from the Rhinestone dashboard**
|
|
276
|
+
|
|
277
|
+
Create a Rhinestone app in the [Rhinestone dashboard](https://dashboard.rhinestone.dev) to generate a JWT signing key, then copy these values into your **server** environment (never the browser):
|
|
278
|
+
|
|
279
|
+
| Variable | Description |
|
|
280
|
+
|----------|-------------|
|
|
281
|
+
| `RHINESTONE_JWT_PRIVATE_KEY` | JSON-encoded JWK (EC P-256 / `ES256` recommended, RSA accepted) |
|
|
282
|
+
| `RHINESTONE_INTEGRATOR_ID` | Your integrator handle — becomes the JWT `iss` |
|
|
283
|
+
| `RHINESTONE_PROJECT_ID` | Project the app belongs to — becomes the JWT `sub` |
|
|
284
|
+
| `RHINESTONE_APP_ID` | App ID — becomes the JWT `app_id` claim |
|
|
285
|
+
| `RHINESTONE_KEY_ID` | Key ID registered with the orchestrator — the `kid` header |
|
|
286
|
+
|
|
287
|
+
`createSponsorshipSigner()` reads all five automatically; pass `{ credentials: { ... } }` to supply them explicitly instead.
|
|
288
|
+
|
|
289
|
+
**2. Create two endpoints on your backend, on the same origin as your app**
|
|
175
290
|
|
|
176
291
|
Gate these with your existing session auth — the user's cookie rides along automatically.
|
|
177
292
|
|
|
@@ -215,7 +330,7 @@ export async function handleExtensionToken(req, res) {
|
|
|
215
330
|
}
|
|
216
331
|
```
|
|
217
332
|
|
|
218
|
-
**
|
|
333
|
+
**3. Configure `OneAuthClient` with the URL form**
|
|
219
334
|
|
|
220
335
|
```typescript
|
|
221
336
|
import { OneAuthClient, createOneAuthProvider } from '@rhinestone/1auth';
|