@rhinestone/1auth 0.6.10 → 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.
Files changed (2) hide show
  1. package/README.md +148 -33
  2. 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 signer for app-sponsored intents |
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
- passkeyServerUrl: 'https://passkey.1auth.box',
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
- intent={intent}
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 { createJwtSigner } from '@rhinestone/1auth/server';
102
+ import { createSponsorshipSigner } from '@rhinestone/1auth/server';
91
103
 
92
- const signer = createJwtSigner({
93
- privateKey: JSON.parse(process.env.RHINESTONE_JWT_PRIVATE_KEY),
94
- integratorId: process.env.RHINESTONE_INTEGRATOR_ID,
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.getIntentExtensionToken(intentInput);
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
- - `getSupportedChains()` - Get list of supported chains
113
- - `getSupportedTokens()` - Get list of supported tokens
114
- - Type exports for intents, accounts, and more
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
- - `BatchQueueProvider` - Context provider for batch operations
120
- - `BatchQueueWidget` - UI widget for batch queue management
121
- - `useBatchQueue` - Hook for batch queue operations
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
- - `createJwtSigner()` - Create a JWT signer for app-sponsored intents (RS256)
126
- - `JwtSignerConfig` - Configuration type for the JWT signer
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
- By default, 1auth sponsors transaction fees for all intents. With sponsorship, your app takes over billing by providing JWT tokens to authorize each intent.
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. Create two endpoints on your backend, on the same origin as your app**
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
- **2. Configure `OneAuthClient` with the URL form**
333
+ **3. Configure `OneAuthClient` with the URL form**
219
334
 
220
335
  ```typescript
221
336
  import { OneAuthClient, createOneAuthProvider } from '@rhinestone/1auth';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinestone/1auth",
3
- "version": "0.6.10",
3
+ "version": "0.7.1",
4
4
  "description": "Passkey-based authentication SDK for Web3 applications",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",