Skip to content

Commit 1b44e3f

Browse files
rvagghugomrdias
andauthored
docs: add session keys developer guide (#630)
Co-authored-by: Hugo Dias <hugomrdias@gmail.com>
1 parent ee7bb7e commit 1b44e3f

4 files changed

Lines changed: 320 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Run `pnpm run lint:fix` before commits.
9494

9595
Storage uses a store → pull → commit pipeline for multi-copy durability:
9696

97-
```
97+
```text
9898
Client ──store──> Primary SP (endorsed)
9999
100100
┌─────────┴─────────┐

docs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
"@hugomrdias/docs": "^0.1.10",
2626
"@types/react": "catalog:",
2727
"@types/react-dom": "catalog:",
28-
"astro": "^5.17.1",
28+
"astro": "^5.18.0",
2929
"astro-mermaid": "^1.1.0",
30-
"expressive-code-twoslash": "^0.5.3",
30+
"expressive-code-twoslash": "^0.6.0",
3131
"mermaid": "^11.12.2",
3232
"rehype-external-links": "^3.0.0",
3333
"sharp": "^0.34.4",
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
---
2+
title: Session Keys
3+
description: Delegate signing permissions to ephemeral keys for improved UX and security.
4+
sidebar:
5+
order: 5
6+
---
7+
8+
## What are session keys?
9+
10+
Session keys are ephemeral signing keys that can perform a limited set of operations on behalf of a root wallet. They are registered on-chain via the **SessionKeyRegistry** contract, which stores permission grants as time-limited authorizations.
11+
12+
This solves a common problem in dApps: without session keys, every storage operation (creating a dataset, uploading pieces, scheduling deletions) requires the user to approve a wallet popup. With session keys, the root wallet authorizes a temporary key once, and that key handles subsequent signing silently until it expires.
13+
14+
### Key concepts
15+
16+
- **Root wallet** - The user's primary wallet (e.g., MetaMask). Owns the identity, funds, and datasets. Used to authorize session keys via `login()`.
17+
- **Session key** - An ephemeral key pair that signs operations on behalf of the root wallet. Has no funds or on-chain identity of its own.
18+
- **Permissions** - Each authorization grants specific operation types until an expiry timestamp. Permissions are identified by `bytes32` hashes (by convention, EIP-712 type hashes).
19+
- **Expiry** - Authorizations are time-limited. The SDK defaults to 1 hour; the contract stores whatever expiry is provided.
20+
21+
### Permissions
22+
23+
The SessionKeyRegistry stores arbitrary `bytes32` hashes as permissions and is agnostic to what they represent. By convention, the SDK uses EIP-712 type hashes to identify operations:
24+
25+
| Constant | Operation |
26+
| ---------- | ----------- |
27+
| `CreateDataSetPermission` | Create new datasets |
28+
| `AddPiecesPermission` | Add pieces to datasets |
29+
| `SchedulePieceRemovalsPermission` | Schedule piece removals |
30+
| `DeleteDataSetPermission` | Delete datasets |
31+
32+
These are the constants currently supported by FWSS. The `Permission` type also accepts any `Hex` value, allowing registration of custom permission hashes for non-FWSS operations (e.g., authenticated Curio HTTP endpoints).
33+
34+
## Quick start
35+
36+
A complete session key lifecycle from creation through to use:
37+
38+
```ts twoslash
39+
import * as SessionKey from '@filoz/synapse-core/session-key'
40+
import { calibration } from '@filoz/synapse-core/chains'
41+
import { createWalletClient, http, type Hex } from 'viem'
42+
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
43+
44+
// The root wallet (the user's primary wallet)
45+
const rootAccount = privateKeyToAccount('0x<root-private-key>' as Hex)
46+
const rootClient = createWalletClient({
47+
account: rootAccount,
48+
chain: calibration,
49+
transport: http(),
50+
})
51+
52+
// Create an ephemeral session key
53+
const privateKey = generatePrivateKey()
54+
const sessionKey = SessionKey.fromSecp256k1({
55+
privateKey,
56+
root: rootAccount, // Account or Address
57+
chain: calibration,
58+
})
59+
60+
// Authorize the session key on-chain (root wallet signs this tx)
61+
const { event } = await SessionKey.loginSync(rootClient, {
62+
address: sessionKey.address,
63+
onHash(hash) {
64+
console.log('Tx submitted:', hash)
65+
},
66+
})
67+
68+
// Sync expirations from the chain so hasPermission() works locally
69+
await sessionKey.syncExpirations()
70+
71+
// Now use sessionKey.client in place of rootClient for SDK operations.
72+
// sessionKey.client signs with the session key; sessionKey.rootAddress
73+
// identifies the root wallet as the payer/identity.
74+
```
75+
76+
## Detailed usage
77+
78+
### Create a session key
79+
80+
`fromSecp256k1()` creates a session key from a secp256k1 private key. It returns a `SessionKey<'Secp256k1'>` instance.
81+
82+
```ts twoslash
83+
import * as SessionKey from '@filoz/synapse-core/session-key'
84+
import { calibration } from '@filoz/synapse-core/chains'
85+
import type { Hex } from 'viem'
86+
import { privateKeyToAccount } from 'viem/accounts'
87+
88+
const rootAccount = privateKeyToAccount('0x<root-private-key>' as Hex)
89+
90+
const sessionKey = SessionKey.fromSecp256k1({
91+
privateKey: '0x...' as Hex, // secp256k1 private key for the session key
92+
root: rootAccount, // Account or Address of the authorizing wallet
93+
chain: calibration, // chain definition (calibration or mainnet)
94+
// transport: http(customRpc), // optional, defaults to http()
95+
// expirations: { ... }, // optional, pre-populate known expirations
96+
})
97+
```
98+
99+
The session key is inert until authorized. It holds a viem `Client` internally (`sessionKey.client`) that uses the session key for signing and carries `sessionKey.rootAddress` as the identity.
100+
101+
### Authorize the session key (login)
102+
103+
The **root wallet** authorizes the session key on-chain. `login()` and `loginSync()` both require a viem `WalletClient` (a `Client` with an `Account`):
104+
105+
```ts twoslash
106+
import * as SessionKey from '@filoz/synapse-core/session-key'
107+
import { calibration } from '@filoz/synapse-core/chains'
108+
import { createWalletClient, http, type Hex } from 'viem'
109+
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
110+
111+
// The root wallet (the user's primary wallet)
112+
const rootAccount = privateKeyToAccount('0x<root-private-key>' as Hex)
113+
const rootClient = createWalletClient({
114+
account: rootAccount,
115+
chain: calibration,
116+
transport: http(),
117+
})
118+
119+
// Create an ephemeral session key
120+
const privateKey = generatePrivateKey()
121+
const sessionKey = SessionKey.fromSecp256k1({
122+
privateKey,
123+
root: rootAccount, // Account or Address
124+
chain: calibration,
125+
})
126+
// ---cut---
127+
// Fire-and-forget (returns tx hash: Hex)
128+
const hash = await SessionKey.login(rootClient, {
129+
address: sessionKey.address,
130+
})
131+
132+
// Or wait for confirmation (returns { receipt, event })
133+
const { receipt, event } = await SessionKey.loginSync(rootClient, {
134+
address: sessionKey.address,
135+
expiresAt: BigInt(Math.floor(Date.now() / 1000) + 7200), // 2 hours
136+
onHash(hash) {
137+
console.log('Tx submitted:', hash)
138+
},
139+
})
140+
// event is the AuthorizationsUpdated log with args: { identity, permissions, expiry }
141+
```
142+
143+
By default, `login()` grants all four [FWSS permissions](#permissions) (`DefaultFwssPermissions`) with a 1-hour expiry. Both `permissions` and `expiresAt` are configurable.
144+
145+
To grant only specific permissions:
146+
147+
```ts
148+
await SessionKey.login(rootClient, {
149+
address: sessionKey.address,
150+
permissions: [
151+
SessionKey.AddPiecesPermission,
152+
SessionKey.SchedulePieceRemovalsPermission,
153+
],
154+
})
155+
```
156+
157+
To grant a custom (non-FWSS) permission:
158+
159+
```ts
160+
await SessionKey.login(rootClient, {
161+
address: sessionKey.address,
162+
permissions: [
163+
'0xabcdef...' as Hex, // any bytes32 hash
164+
],
165+
})
166+
```
167+
168+
### Use the session key for operations
169+
170+
Pass `sessionKey.client` to SDK operations. The session key signs the EIP-712 typed data while `sessionKey.rootAddress` is used as the payer/identity:
171+
172+
```ts
173+
import { createDataSet, waitForCreateDataSet } from '@filoz/synapse-core/sp'
174+
175+
const result = await createDataSet(sessionKey.client, {
176+
payee: providerAddress, // Address: the SP's address
177+
payer: sessionKey.rootAddress, // Address: the root wallet paying for storage
178+
serviceURL: 'https://provider.example.com',
179+
})
180+
181+
const dataset = await waitForCreateDataSet(result)
182+
```
183+
184+
### Use with the Synapse class
185+
186+
The `Synapse` class accepts a `sessionKey` option (`SessionKey<'Secp256k1'>`) and uses it automatically for eligible operations (dataset creation, piece uploads, piece deletions):
187+
188+
```ts
189+
import { Synapse } from '@filoz/synapse-sdk'
190+
191+
const synapse = Synapse.create({
192+
account: rootAccount,
193+
chain: calibration,
194+
transport: http(rpcUrl),
195+
sessionKey: sessionKey,
196+
})
197+
```
198+
199+
`Synapse.create()` validates that the session key has all four FWSS permissions (`DefaultFwssPermissions`) and that none are expired. This means the session key's expirations must be populated before construction, either by passing `expirations` to `fromSecp256k1()`, or by calling `sessionKey.syncExpirations()` after login.
200+
201+
### Revoke the session key
202+
203+
When done, the root wallet can revoke permissions:
204+
205+
```ts
206+
// Fire-and-forget (returns tx hash: Hex)
207+
const hash = await SessionKey.revoke(rootClient, {
208+
address: sessionKey.address,
209+
})
210+
211+
// Or wait for confirmation (returns { receipt, event })
212+
await SessionKey.revokeSync(rootClient, {
213+
address: sessionKey.address,
214+
onHash(hash) {
215+
console.log('Revoking:', hash)
216+
},
217+
})
218+
```
219+
220+
Both default to revoking all FWSS permissions. Pass `permissions` to revoke selectively.
221+
222+
## Expirations and refresh
223+
224+
Session key permissions have a fixed expiry set during `login()`. When a permission expires, any operation signed with that session key will revert on-chain.
225+
226+
The SDK does not automatically track or refresh expirations. For short-lived sessions (login, perform operations, done), this is not a concern. For long-lived sessions, the developer should:
227+
228+
- Check `sessionKey.hasPermission(permission)` before operations if expirations are populated
229+
- Call `sessionKey.syncExpirations()` periodically to refresh cached state from the chain
230+
- Call `login()` again from the root wallet when permissions are near expiry
231+
232+
Errors from expired session keys will surface as contract reverts. The SDK does not currently distinguish these from other revert causes.
233+
234+
### Checking permissions
235+
236+
`hasPermission()` and `hasPermissions()` are local checks against cached expiration timestamps. They return `true` if the permission's expiry is in the future:
237+
238+
```ts
239+
// Check a single permission (returns boolean)
240+
if (sessionKey.hasPermission(SessionKey.CreateDataSetPermission)) {
241+
// safe to create dataset
242+
}
243+
244+
// Check all FWSS permissions at once (returns boolean)
245+
if (sessionKey.hasPermissions(SessionKey.DefaultFwssPermissions)) {
246+
// all FWSS permissions are valid
247+
}
248+
```
249+
250+
These require that expirations have been populated via one of:
251+
252+
- `fromSecp256k1({ expirations: ... })` at creation time
253+
- `sessionKey.syncExpirations()` (fetches from chain via multicall)
254+
- `sessionKey.watch()` (syncs and subscribes to live updates)
255+
256+
### Real-time tracking (optional)
257+
258+
For dApps that need live permission state (e.g., to update UI when permissions expire or are revoked):
259+
260+
```ts
261+
const unwatch = await sessionKey.watch()
262+
263+
sessionKey.on('expirationsUpdated', (e: CustomEvent<Expirations>) => {
264+
console.log('Permissions changed:', e.detail)
265+
})
266+
267+
sessionKey.on('error', (e: CustomEvent<Error>) => {
268+
console.error('Watch error:', e.detail)
269+
})
270+
271+
// When done, clean up the subscription
272+
unwatch()
273+
// or: sessionKey.unwatch()
274+
```
275+
276+
`watch()` syncs expirations from the chain, starts a `watchContractEvent` subscription for `AuthorizationsUpdated` events, and returns a cleanup function. You can also call `sessionKey.unwatch()` directly. This is primarily useful for dApp UI; server-side code can use `syncExpirations()` directly.
277+
278+
## Custom permissions
279+
280+
The four FWSS constants are SDK conveniences, not an exhaustive set. Any `bytes32` hash can be registered as a permission. To work with custom permissions:
281+
282+
```ts
283+
import * as SessionKey from '@filoz/synapse-core/session-key'
284+
import { createPublicClient, http, type Hex } from 'viem'
285+
import { calibration } from '@filoz/synapse-core/chains'
286+
287+
const publicClient = createPublicClient({
288+
chain: calibration,
289+
transport: http(),
290+
})
291+
292+
const myPermission = '0x...' as Hex
293+
294+
// Grant (requires root wallet client)
295+
await SessionKey.login(rootClient, {
296+
address: sessionKey.address,
297+
permissions: [myPermission],
298+
})
299+
300+
// Check single expiry (returns bigint, 0n if no authorization exists)
301+
const expiry = await SessionKey.authorizationExpiry(publicClient, {
302+
address: rootAddress, // Address: the root wallet
303+
sessionKeyAddress: sessionKey.address,
304+
permission: myPermission,
305+
})
306+
307+
// Batch check (returns Record<Permission, bigint>)
308+
const expirations = await SessionKey.getExpirations(publicClient, {
309+
address: rootAddress,
310+
sessionKeyAddress: sessionKey.address,
311+
permissions: [myPermission, SessionKey.AddPiecesPermission],
312+
})
313+
```
314+
315+
## Next Steps
316+
317+
- [Session Keys API](/reference/filoz/synapse-core/session-key/toc/#functions): Reference documentation

packages/synapse-sdk/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"entryPoints": [
1616
"src/index.ts",
1717
"src/payments/index.ts",
18-
"src/session/index.ts",
1918
"src/storage/index.ts",
2019
"src/warm-storage/index.ts",
2120
"src/sp-registry/index.ts",

0 commit comments

Comments
 (0)