Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions docs/src/content/docs/developer-guides/session-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,30 @@ const synapse = Synapse.create({
})
```

`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.
By default, `Synapse.create()` validates that the session key has all four FWSS permissions (`SessionKey.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.

#### All-or-nothing in `@filoz/synapse-sdk`
The error thrown by `Synapse.create()` names the specific permissions that are missing or expired (distinguishing "not authorized" from "expired at <timestamp>") so you can decide whether to extend the session key's authorizations or narrow the required set.

`@filoz/synapse-sdk` is the high-level "golden path" entry point and deliberately takes an all-or-nothing stance on session key permissions: `Synapse.create()` will throw if any of `CreateDataSet`, `AddPieces`, `SchedulePieceRemovals`, or `TerminateService` is missing or expired. This keeps the API surface predictable — every operation the high-level SDK exposes will work once construction succeeds.
#### Narrowing the required scope

If you want a narrower scope (for example, only `CreateDataSet` + `AddPieces` for an upload-only client), drop down to [`@filoz/synapse-core`](/developer-guides/synapse-core/) directly. The core package's `SessionKey` and SP-client functions check permissions per operation, so you can authorize the minimum set you need and call those operations directly without going through `Synapse.create()`.
By default `@filoz/synapse-sdk` takes an all-or-nothing stance: every operation the high-level SDK exposes works once construction succeeds. If your app only exercises a subset of those operations — for example an upload-only client that needs `CreateDataSet` + `AddPieces` but never schedules removals or terminates services — pass `requiredPermissions` so that `Synapse.create()` validates only what you actually use:

The error thrown by `Synapse.create()` names the specific permissions that are missing or expired so you can decide whether to extend the session key's authorizations or switch to `@filoz/synapse-core` for that flow.
```ts
const synapse = Synapse.create({
account: rootAccount,
chain: calibration,
transport: http(rpcUrl),
sessionKey: sessionKey,
requiredPermissions: [
SessionKey.CreateDataSetPermission,
SessionKey.AddPiecesPermission,
],
})
```

`requiredPermissions` defaults to `SessionKey.DefaultFwssPermissions`, so existing callers see no behavior change. Note that `requiredPermissions` only gates construction — the SDK does **not** enforce per-operation checks. Calling an SDK method whose permission is not in `requiredPermissions` will still go through, and if the session key is not authorized for that operation it will revert on-chain. Use `requiredPermissions` to gate construction, and only call SDK methods you have authorized.

If you want per-operation permission enforcement, drop down to [`@filoz/synapse-core`](/developer-guides/synapse-core/) directly. The core package's `SessionKey` and SP-client functions check permissions per operation, so you can authorize the minimum set you need and call those operations directly without going through `Synapse.create()`.

### Revoke the session key

Expand Down
24 changes: 13 additions & 11 deletions packages/synapse-sdk/src/synapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,23 @@ export class Synapse {

if (options.sessionKey != null) {
const sessionKey = options.sessionKey
const missing = SessionKey.DefaultFwssPermissions.filter(
(permission) => !sessionKey.hasPermission(permission)
).map((permission) => {
const name = SessionKey.PermissionNames[permission] ?? permission
const expiry = sessionKey.expirations[permission]
if (expiry == null || expiry === 0n) {
return `${name} (not authorized)`
}
return `${name} (expired at ${new Date(Number(expiry) * 1000).toISOString()})`
})
const requiredPermissions = options.requiredPermissions ?? SessionKey.DefaultFwssPermissions
const missing = requiredPermissions
.filter((permission) => !sessionKey.hasPermission(permission))
.map((permission) => {
const name = SessionKey.PermissionNames[permission] ?? permission
const expiry = sessionKey.expirations[permission]
if (expiry == null || expiry === 0n) {
return `${name} (not authorized)`
}
return `${name} (expired at ${new Date(Number(expiry) * 1000).toISOString()})`
})
if (missing.length > 0) {
throw new Error(
`Session key is missing required FWSS permissions: ${missing.join(', ')}. ` +
'Synapse.create requires every permission in SessionKey.DefaultFwssPermissions to be authorized and unexpired. ' +
'Synapse.create requires every permission in requiredPermissions (defaults to SessionKey.DefaultFwssPermissions) to be authorized and unexpired. ' +
'Authorize the session key for all of them (SessionKey.login) and refresh local state (sessionKey.syncExpirations), ' +
'pass a narrower requiredPermissions set, ' +
'or drop down to @filoz/synapse-core to operate with a custom permission scope. ' +
'See https://docs.filecoin.cloud/developer-guides/session-keys/ for details.'
)
Expand Down
87 changes: 78 additions & 9 deletions packages/synapse-sdk/src/test/session-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,18 @@ describe('Synapse', () => {
})

describe('Synapse.create permission validation', () => {
const now = () => BigInt(Math.floor(Date.now() / 1000))

it('should throw an informative error listing not-authorized and expired permissions', () => {
const now = BigInt(Math.floor(Date.now() / 1000))
const sessionKey = SessionKey.fromSecp256k1({
chain: calibration,
privateKey: Mocks.PRIVATE_KEYS.key2,
root: client.account,
expirations: {
[SessionKey.CreateDataSetPermission]: now + 3600n,
[SessionKey.CreateDataSetPermission]: now() + 3600n,
[SessionKey.AddPiecesPermission]: 0n,
[SessionKey.SchedulePieceRemovalsPermission]: now - 3600n,
[SessionKey.TerminateServicePermission]: now + 3600n,
[SessionKey.SchedulePieceRemovalsPermission]: now() - 3600n,
[SessionKey.TerminateServicePermission]: now() + 3600n,
},
})

Expand All @@ -239,22 +240,90 @@ describe('Synapse', () => {

it('should not throw when all FWSS permissions are valid', () => {
server.use(Mocks.JSONRPC(Mocks.presets.basic))
const now = BigInt(Math.floor(Date.now() / 1000))
const sessionKey = SessionKey.fromSecp256k1({
chain: calibration,
privateKey: Mocks.PRIVATE_KEYS.key2,
root: client.account,
expirations: {
[SessionKey.CreateDataSetPermission]: now + 3600n,
[SessionKey.AddPiecesPermission]: now + 3600n,
[SessionKey.SchedulePieceRemovalsPermission]: now + 3600n,
[SessionKey.TerminateServicePermission]: now + 3600n,
[SessionKey.CreateDataSetPermission]: now() + 3600n,
[SessionKey.AddPiecesPermission]: now() + 3600n,
[SessionKey.SchedulePieceRemovalsPermission]: now() + 3600n,
[SessionKey.TerminateServicePermission]: now() + 3600n,
},
})

const synapse = Synapse.create({ chain: calibration, account, source: null, sessionKey })
assert.exists(synapse)
})

it('should succeed when only the narrower required scope is authorized', () => {
server.use(Mocks.JSONRPC(Mocks.presets.basic))
const sessionKey = SessionKey.fromSecp256k1({
chain: calibration,
privateKey: Mocks.PRIVATE_KEYS.key2,
root: client.account,
expirations: {
[SessionKey.CreateDataSetPermission]: now() + 3600n,
[SessionKey.AddPiecesPermission]: now() + 3600n,
[SessionKey.SchedulePieceRemovalsPermission]: 0n,
[SessionKey.TerminateServicePermission]: 0n,
},
})

const synapse = Synapse.create({
chain: calibration,
account,
source: null,
sessionKey,
requiredPermissions: [SessionKey.CreateDataSetPermission, SessionKey.AddPiecesPermission],
})
assert.exists(synapse)
})

it('should still throw when a required permission is missing from the narrower set', () => {
const sessionKey = SessionKey.fromSecp256k1({
chain: calibration,
privateKey: Mocks.PRIVATE_KEYS.key2,
root: client.account,
expirations: {
[SessionKey.CreateDataSetPermission]: now() + 3600n,
[SessionKey.AddPiecesPermission]: 0n,
[SessionKey.SchedulePieceRemovalsPermission]: now() + 3600n,
[SessionKey.TerminateServicePermission]: now() + 3600n,
},
})

assert.throws(
() =>
Synapse.create({
chain: calibration,
account,
source: null,
sessionKey,
requiredPermissions: [SessionKey.CreateDataSetPermission, SessionKey.AddPiecesPermission],
}),
/Session key is missing required FWSS permissions/
)
})

it('should default to DefaultFwssPermissions when requiredPermissions is omitted', () => {
const sessionKey = SessionKey.fromSecp256k1({
chain: calibration,
privateKey: Mocks.PRIVATE_KEYS.key2,
root: client.account,
expirations: {
[SessionKey.CreateDataSetPermission]: now() + 3600n,
[SessionKey.AddPiecesPermission]: now() + 3600n,
[SessionKey.SchedulePieceRemovalsPermission]: 0n,
[SessionKey.TerminateServicePermission]: 0n,
},
})

assert.throws(
() => Synapse.create({ chain: calibration, account, source: null, sessionKey }),
/Session key is missing required FWSS permissions/
)
})
})
})
})
18 changes: 17 additions & 1 deletion packages/synapse-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { Chain } from '@filoz/synapse-core/chains'
import type { PieceCID } from '@filoz/synapse-core/piece'
import type { SessionKey, SessionKeyAccount } from '@filoz/synapse-core/session-key'
import type { Permission, SessionKey, SessionKeyAccount } from '@filoz/synapse-core/session-key'
import type { pullPiecesApiRequest } from '@filoz/synapse-core/sp'
import type { PDPProvider } from '@filoz/synapse-core/sp-registry'
import type { MetadataObject } from '@filoz/synapse-core/utils'
Expand Down Expand Up @@ -112,6 +112,22 @@ export interface SynapseOptions {

sessionKey?: SessionKey<'Secp256k1'>

/**
* The set of session key permissions `Synapse.create` validates as authorized and unexpired.
*
* Defaults to `SessionKey.DefaultFwssPermissions` (all four FWSS permissions:
* `CreateDataSet`, `AddPieces`, `SchedulePieceRemovals`, `TerminateService`), which matches
* the operations exposed by the high-level Synapse class.
*
* Pass a narrower array (e.g. `[CreateDataSetPermission, AddPiecesPermission]`) to keep
* least-privilege session keys on the `Synapse.create` happy path when the app only exercises
* a subset of the SDK surface. Operations whose permissions are not listed here will revert
* on-chain if attempted; the SDK does not enforce per-operation checks.
*
* Only meaningful together with `sessionKey`.
*/
requiredPermissions?: Permission[]

/** Whether to use CDN for retrievals (default: false) */
withCDN?: boolean

Expand Down
Loading