Skip to content

Session Keys

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.

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.

  • Root wallet - The user’s primary wallet (e.g., MetaMask). Owns the identity, funds, and datasets. Used to authorize session keys via login().
  • 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.
  • Permissions - Each authorization grants specific operation types until an expiry timestamp. Permissions are identified by bytes32 hashes (by convention, EIP-712 type hashes).
  • Expiry - Authorizations are time-limited. The SDK defaults to 1 hour; the contract stores whatever expiry is provided.

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:

ConstantOperation
CreateDataSetPermissionCreate new datasets
AddPiecesPermissionUpload pieces to datasets
SchedulePieceRemovalsPermissionSchedule piece deletions
DeleteDataSetPermissionDelete datasets

These are convenience constants for FWSS operations. The Permission type also accepts any Hex value, allowing registration of custom permission hashes for non-FWSS operations (e.g., authenticated Curio HTTP endpoints).

A complete session key lifecycle from creation through to use:

import * as SessionKey from '@filoz/synapse-core/session-key'
import { calibration } from '@filoz/synapse-core/chains'
import { createWalletClient, http, type Hex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
// The root wallet (the user's primary wallet)
const rootAccount = privateKeyToAccount('0x<root-private-key>' as Hex)
const rootClient = createWalletClient({
account: rootAccount,
chain: calibration,
transport: http('https://api.calibration.node.glif.io/rpc/v1'),
})
// Create an ephemeral session key
const sessionKey = SessionKey.fromSecp256k1({
privateKey: '0x<session-private-key>' as Hex,
root: rootAccount, // Account or Address
chain: calibration,
})
// Authorize the session key on-chain (root wallet signs this tx)
const { event } = await SessionKey.loginSync(rootClient, {
address: sessionKey.address,
onHash(hash) {
console.log('Tx submitted:', hash)
},
})
// Sync expirations from the chain so hasPermission() works locally
await sessionKey.syncExpirations()
// Now use sessionKey.client in place of rootClient for SDK operations.
// sessionKey.client signs with the session key; sessionKey.rootAddress
// identifies the root wallet as the payer/identity.

fromSecp256k1() creates a session key from a secp256k1 private key. It returns a SessionKey<'Secp256k1'> instance.

import * as SessionKey from '@filoz/synapse-core/session-key'
import { calibration } from '@filoz/synapse-core/chains'
import type { Hex } from 'viem'
const sessionKey = SessionKey.fromSecp256k1({
privateKey: '0x...' as Hex, // secp256k1 private key for the session key
root: rootAccount, // Account or Address of the authorizing wallet
chain: calibration, // chain definition (calibration or mainnet)
// transport: http(customRpc), // optional, defaults to http()
// expirations: { ... }, // optional, pre-populate known expirations
})

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.

The root wallet authorizes the session key on-chain. login() and loginSync() both require a viem WalletClient (a Client with an Account):

// Fire-and-forget (returns tx hash: Hex)
const hash = await SessionKey.login(rootClient, {
address: sessionKey.address,
})
// Or wait for confirmation (returns { receipt, event })
const { receipt, event } = await SessionKey.loginSync(rootClient, {
address: sessionKey.address,
expiresAt: BigInt(Math.floor(Date.now() / 1000) + 7200), // 2 hours
onHash(hash) {
console.log('Tx submitted:', hash)
},
})
// event is the AuthorizationsUpdated log with args: { identity, permissions, expiry }

By default, login() grants all four FWSS permissions (DefaultFwssPermissions) with a 1-hour expiry. Both permissions and expiresAt are configurable.

To grant only specific permissions:

await SessionKey.login(rootClient, {
address: sessionKey.address,
permissions: [
SessionKey.AddPiecesPermission,
SessionKey.SchedulePieceRemovalsPermission,
],
})

To grant a custom (non-FWSS) permission:

await SessionKey.login(rootClient, {
address: sessionKey.address,
permissions: [
'0xabcdef...' as Hex, // any bytes32 hash
],
})

Pass sessionKey.client to SDK operations. The session key signs the EIP-712 typed data while sessionKey.rootAddress is used as the payer/identity:

import { createDataSet, waitForCreateDataSet } from '@filoz/synapse-core/sp'
const result = await createDataSet(sessionKey.client, {
payee: providerAddress, // Address: the SP's address
payer: sessionKey.rootAddress, // Address: the root wallet paying for storage
serviceURL: 'https://provider.example.com',
})
const dataset = await waitForCreateDataSet(result)

The Synapse class accepts a sessionKey option (SessionKey<'Secp256k1'>) and uses it automatically for eligible operations (dataset creation, piece uploads, piece deletions):

import { Synapse } from '@filoz/synapse-sdk'
const synapse = Synapse.create({
account: rootAccount,
chain: calibration,
transport: http(rpcUrl),
sessionKey: sessionKey,
})

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.

When done, the root wallet can revoke permissions:

// Fire-and-forget (returns tx hash: Hex)
const hash = await SessionKey.revoke(rootClient, {
address: sessionKey.address,
})
// Or wait for confirmation (returns { receipt, event })
await SessionKey.revokeSync(rootClient, {
address: sessionKey.address,
onHash(hash) {
console.log('Revoking:', hash)
},
})

Both default to revoking all FWSS permissions. Pass permissions to revoke selectively.

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.

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:

  • Check sessionKey.hasPermission(permission) before operations if expirations are populated
  • Call sessionKey.syncExpirations() periodically to refresh cached state from the chain
  • Call login() again from the root wallet when permissions are near expiry

Errors from expired session keys will surface as contract reverts. The SDK does not currently distinguish these from other revert causes.

hasPermission() and hasPermissions() are local checks against cached expiration timestamps. They return true if the permission’s expiry is in the future:

// Check a single permission (returns boolean)
if (sessionKey.hasPermission(SessionKey.CreateDataSetPermission)) {
// safe to create dataset
}
// Check all FWSS permissions at once (returns boolean)
if (sessionKey.hasPermissions(SessionKey.DefaultFwssPermissions)) {
// all FWSS permissions are valid
}

These require that expirations have been populated via one of:

  • fromSecp256k1({ expirations: ... }) at creation time
  • sessionKey.syncExpirations() (fetches from chain via multicall)
  • sessionKey.watch() (syncs and subscribes to live updates)

For dApps that need live permission state (e.g., to update UI when permissions expire or are revoked):

const unwatch = await sessionKey.watch()
sessionKey.on('expirationsUpdated', (e: CustomEvent<Expirations>) => {
console.log('Permissions changed:', e.detail)
})
sessionKey.on('error', (e: CustomEvent<Error>) => {
console.error('Watch error:', e.detail)
})
// When done, clean up the subscription
unwatch()
// or: sessionKey.unwatch()

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.

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:

import * as SessionKey from '@filoz/synapse-core/session-key'
import { createPublicClient, http, type Hex } from 'viem'
import { calibration } from '@filoz/synapse-core/chains'
const publicClient = createPublicClient({
chain: calibration,
transport: http(),
})
const myPermission = '0x...' as Hex
// Grant (requires root wallet client)
await SessionKey.login(rootClient, {
address: sessionKey.address,
permissions: [myPermission],
})
// Check single expiry (returns bigint, 0n if no authorization exists)
const expiry = await SessionKey.authorizationExpiry(publicClient, {
address: rootAddress, // Address: the root wallet
sessionKeyAddress: sessionKey.address,
permission: myPermission,
})
// Batch check (returns Record<Permission, bigint>)
const expirations = await SessionKey.getExpirations(publicClient, {
address: rootAddress,
sessionKeyAddress: sessionKey.address,
permissions: [myPermission, SessionKey.AddPiecesPermission],
})

Create a session key with its own viem client.

  • options.privateKey Hex - secp256k1 private key
  • options.root Account | Address - the authorizing wallet
  • options.chain Chain - chain definition
  • options.transport? Transport - defaults to http()
  • options.expirations? Expirations - pre-populate known expirations
  • Returns SessionKey<'Secp256k1'>

accountFromSecp256k1(options) {#accountFromSecp256k1}

Section titled “accountFromSecp256k1(options) {#accountFromSecp256k1}”

Create a session key account without the wrapper (for custom client setup).

  • options.privateKey Hex - secp256k1 private key
  • options.rootAddress Address - the authorizing wallet’s address
  • Returns SessionKeyAccount<'Secp256k1'>

Properties:

  • .client Client<Transport, Chain, SessionKeyAccount<'Secp256k1'>> - viem client that signs with the session key
  • .address Address - the session key’s address
  • .rootAddress Address - the authorizing wallet’s address
  • .expirations Record<Permission, bigint> - cached permission expiry timestamps

Methods:

  • .hasPermission(permission) - returns boolean, local check against cached expirations
  • .hasPermissions(permissions) - returns boolean, checks all permissions are valid
  • .syncExpirations() - returns Promise<void>, fetches expirations from chain via multicall
  • .watch() - returns Promise<() => void>, syncs + subscribes to live AuthorizationsUpdated events; the returned function stops watching
  • .unwatch() - stops watching for live events

All write operations require a wallet client (Client<Transport, Chain, Account>).

Authorize a session key on-chain. Returns Hash.

  • options.address Address - session key address
  • options.permissions? Permission[] - defaults to DefaultFwssPermissions
  • options.expiresAt? bigint - unix timestamp, defaults to now + 1 hour
  • options.origin? string - defaults to 'synapse'

Authorize and wait for confirmation. Returns { receipt, event }.

Same options as login(), plus:

  • options.onHash? (hash: Hash) => void - called when tx is submitted

Revoke session key permissions. Returns Hash.

  • options.address Address - session key address
  • options.permissions? Permission[] - defaults to DefaultFwssPermissions
  • options.origin? string - defaults to 'synapse'

Revoke and wait for confirmation. Returns { receipt, event }.

Same options as revoke(), plus:

  • options.onHash? (hash: Hash) => void - called when tx is submitted

Read operations require a public client (Client<Transport, Chain>).

authorizationExpiry(client, options) {#authorizationExpiry}

Section titled “authorizationExpiry(client, options) {#authorizationExpiry}”

Get the expiry timestamp for a single permission. Returns bigint (0n if no authorization exists).

  • options.address Address - the root wallet
  • options.sessionKeyAddress Address - the session key
  • options.permission Permission - which permission to check

Check if a permission is expired. Returns boolean.

Same options as authorizationExpiry().

getExpirations(client, options) {#getExpirations}

Section titled “getExpirations(client, options) {#getExpirations}”

Batch-fetch expirations for multiple permissions. Returns Record<Permission, bigint>.

  • options.address Address - the root wallet
  • options.sessionKeyAddress Address - the session key
  • options.permissions? Permission[] - defaults to DefaultFwssPermissions