From 9bdd9387d64878cee6e59ef5ec1e59d91a891a49 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Thu, 28 May 2026 15:31:01 +0200 Subject: [PATCH] feat(synapse): add requiredPermissions option to SynapseOptions Synapse.create previously hard-coded its session key check to DefaultFwssPermissions (all four FWSS permissions). Apps with a least-privilege session key (for example an upload-only client scoped to CreateDataSet + AddPieces) had to bypass Synapse.create and use the @filoz/synapse-core constructor directly, which meant re-implementing the viem client + transport defaults that Synapse.create provides. Add an optional requiredPermissions?: Permission[] field on SynapseOptions, defaulting to SessionKey.DefaultFwssPermissions. Callers that need a narrower scope can pass it directly: Synapse.create({ ..., sessionKey, requiredPermissions: [ SessionKey.CreateDataSetPermission, SessionKey.AddPiecesPermission, ], }) The SDK still does not enforce per-operation permission checks -- calling an SDK method whose permission is not in requiredPermissions will go through and revert on-chain if the session key is not authorized. requiredPermissions only gates Synapse.create. Existing callers see no behavior change. Per https://github.com/FilOzone/synapse-sdk/issues/695#issuecomment-4421842036. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/developer-guides/session-keys.mdx | 19 +++++ packages/synapse-sdk/src/synapse.ts | 11 ++- .../synapse-sdk/src/test/session-keys.test.ts | 73 +++++++++++++++++++ packages/synapse-sdk/src/types.ts | 18 ++++- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/developer-guides/session-keys.mdx b/docs/src/content/docs/developer-guides/session-keys.mdx index 472fee2fd..0cbb19a6f 100644 --- a/docs/src/content/docs/developer-guides/session-keys.mdx +++ b/docs/src/content/docs/developer-guides/session-keys.mdx @@ -198,6 +198,25 @@ 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. +#### Narrowing the required scope + +If your app only exercises a subset of the SDK's 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: + +```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. 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. + ### Revoke the session key When done, the root wallet can revoke permissions: diff --git a/packages/synapse-sdk/src/synapse.ts b/packages/synapse-sdk/src/synapse.ts index 30f66a55d..c5a4ecdd3 100644 --- a/packages/synapse-sdk/src/synapse.ts +++ b/packages/synapse-sdk/src/synapse.ts @@ -58,10 +58,13 @@ export class Synapse { throw new Error('Transport must be a custom transport. See https://viem.sh/docs/clients/transports/custom.') } - if (options.sessionKey != null && !options.sessionKey.hasPermissions(SessionKey.DefaultFwssPermissions)) { - throw new Error( - 'Session key does not have the required permissions. Please login and sync expirations with the session key first.' - ) + if (options.sessionKey != null) { + const requiredPermissions = options.requiredPermissions ?? SessionKey.DefaultFwssPermissions + if (!options.sessionKey.hasPermissions(requiredPermissions)) { + throw new Error( + 'Session key does not have the required permissions. Please login and sync expirations with the session key first.' + ) + } } return new Synapse({ diff --git a/packages/synapse-sdk/src/test/session-keys.test.ts b/packages/synapse-sdk/src/test/session-keys.test.ts index 998e04940..43377268e 100644 --- a/packages/synapse-sdk/src/test/session-keys.test.ts +++ b/packages/synapse-sdk/src/test/session-keys.test.ts @@ -206,5 +206,78 @@ describe('Synapse', () => { await context.deletePiece({ piece: result.pieceCid }) }) + + describe('Synapse.create requiredPermissions', () => { + const now = () => BigInt(Math.floor(Date.now() / 1000)) + + 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], + }), + /required 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 }), + /required permissions/ + ) + }) + }) }) }) diff --git a/packages/synapse-sdk/src/types.ts b/packages/synapse-sdk/src/types.ts index 46b8c710a..2eb0f252c 100644 --- a/packages/synapse-sdk/src/types.ts +++ b/packages/synapse-sdk/src/types.ts @@ -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' @@ -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