From 2c64ab2aa1f867ca246273288c044b948f681925 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 24 Mar 2026 17:35:28 -0500 Subject: [PATCH 01/10] feat(server): add session key auth flags to server command Co-Authored-By: Claude Sonnet 4.6 --- src/commands/server.ts | 10 +++++++++- src/config.ts | 3 +++ src/core/synapse/index.ts | 3 +++ src/filecoin-pinning-server.ts | 33 ++++++++++++++++++++++++--------- src/server.ts | 9 +++++---- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/commands/server.ts b/src/commands/server.ts index 3a07eb88..31b35ecf 100644 --- a/src/commands/server.ts +++ b/src/commands/server.ts @@ -8,7 +8,9 @@ export const serverCommand = new Command('server') .option('--host ', 'server host', '127.0.0.1') .option('--car-storage ', 'path for CAR file storage', './cars') .option('--database ', 'path to SQLite database', './pins.db') - .option('--private-key ', 'private key for Synapse (or use PRIVATE_KEY env var)') + .option('--private-key ', 'private key for standard auth (or use PRIVATE_KEY env var)') + .option('--wallet-address
', 'wallet address for session key auth (or use WALLET_ADDRESS env var)') + .option('--session-key ', 'session key for session key auth (or use SESSION_KEY env var)') addNetworkOptions(serverCommand) .addOption( @@ -20,6 +22,12 @@ addNetworkOptions(serverCommand) if (options.privateKey) { process.env.PRIVATE_KEY = options.privateKey } + if (options.walletAddress) { + process.env.WALLET_ADDRESS = options.walletAddress + } + if (options.sessionKey) { + process.env.SESSION_KEY = options.sessionKey + } // RPC URL takes precedence over network flag if (options.rpcUrl) { process.env.RPC_URL = options.rpcUrl diff --git a/src/config.ts b/src/config.ts index a5677e1c..6d35dfe3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,6 +50,9 @@ export function createConfig(): Config { // Synapse SDK configuration privateKey: process.env.PRIVATE_KEY, // Required: Ethereum-compatible private key + walletAddress: process.env.WALLET_ADDRESS, + sessionKey: process.env.SESSION_KEY, + viewAddress: process.env.VIEW_ADDRESS, rpcUrl, // Determined from RPC_URL, NETWORK, or default to calibration // Storage paths databasePath: process.env.DATABASE_PATH ?? join(dataDir, 'pins.db'), diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index a7f2d217..1f37293a 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -30,6 +30,9 @@ export interface Config { port: number host: string privateKey: string | undefined + walletAddress: string | undefined + sessionKey: string | undefined + viewAddress: string | undefined rpcUrl: string databasePath: string carStoragePath: string diff --git a/src/filecoin-pinning-server.ts b/src/filecoin-pinning-server.ts index 6d144997..4db193c8 100644 --- a/src/filecoin-pinning-server.ts +++ b/src/filecoin-pinning-server.ts @@ -1,8 +1,9 @@ import fastify, { type FastifyInstance, type FastifyRequest } from 'fastify' import { CID } from 'multiformats/cid' import type { Logger } from 'pino' +import type { Address } from 'viem' import type { Config } from './core/synapse/index.js' -import { initializeSynapse, type PrivateKeyConfig } from './core/synapse/index.js' +import { initializeSynapse, type SynapseSetupConfig } from './core/synapse/index.js' import { FilecoinPinStore, type PinOptions } from './filecoin-pin-store.js' import type { ServiceInfo } from './server.js' @@ -20,20 +21,34 @@ const DEFAULT_USER_INFO = { name: 'Default User', } +function buildSynapseConfig(config: Config): SynapseSetupConfig { + const base = { rpcUrl: config.rpcUrl } + + if (config.walletAddress && config.sessionKey) { + return { + ...base, + walletAddress: config.walletAddress as Address, + sessionKey: config.sessionKey as Address, + } + } + + if (config.privateKey) { + return { ...base, privateKey: config.privateKey as Address } + } + + throw new Error( + 'No authentication configured. Provide a private key (--private-key / PRIVATE_KEY) ' + + 'or session key (--wallet-address + --session-key / WALLET_ADDRESS + SESSION_KEY).' + ) +} + export async function createFilecoinPinningServer( config: Config, logger: Logger, serviceInfo: ServiceInfo ): Promise<{ server: FastifyInstance; pinStore: FilecoinPinStore }> { // Set up Synapse service - if (!config.privateKey) { - throw new Error('PRIVATE_KEY environment variable is required to start the pinning server') - } - - const synapseConfig: PrivateKeyConfig = { - privateKey: config.privateKey as `0x${string}`, - rpcUrl: config.rpcUrl, - } + const synapseConfig = buildSynapseConfig(config) const synapse = await initializeSynapse(synapseConfig, logger) const filecoinPinStore = new FilecoinPinStore({ diff --git a/src/server.ts b/src/server.ts index 039d5311..3e041cca 100644 --- a/src/server.ts +++ b/src/server.ts @@ -60,10 +60,11 @@ export async function startServer(): Promise { ) // Also print a user-friendly message to stderr for clarity - if (errorMessage.includes('PRIVATE_KEY')) { - console.error('\n❌ Error: PRIVATE_KEY environment variable is required') - console.error(' Please set your private key: export PRIVATE_KEY=0x...') - console.error(' Or run with: PRIVATE_KEY=0x... filecoin-pin server\n') + if (errorMessage.includes('No authentication')) { + console.error('\n❌ Error: Authentication is required to start the pinning server') + console.error(' Private key: --private-key or PRIVATE_KEY=0x...') + console.error(' Session key: --wallet-address --session-key ') + console.error(' or WALLET_ADDRESS=0x... SESSION_KEY=0x...\n') } process.exit(1) From 53f6e413d75bea3a954250dc5a1c523e3207eb54 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 25 Mar 2026 14:53:02 -0500 Subject: [PATCH 02/10] refactor(types): use Address, Hex, and Hash instead of `0x${string}` Co-Authored-By: Claude Sonnet 4.6 --- src/core/payments/index.ts | 4 ++-- src/core/piece/remove-piece.ts | 11 ++++++----- src/core/synapse/index.ts | 16 ++++++++-------- src/core/upload/synapse.ts | 3 ++- src/filecoin-pinning-server.ts | 6 +++--- src/payments/interactive.ts | 4 ++-- src/test/unit/remove-piece.test.ts | 5 +++-- 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/core/payments/index.ts b/src/core/payments/index.ts index 120060c1..18d5eeef 100644 --- a/src/core/payments/index.ts +++ b/src/core/payments/index.ts @@ -16,7 +16,7 @@ */ import { calibration, SIZE_CONSTANTS, type Synapse, TIME_CONSTANTS, TOKENS } from '@filoz/synapse-sdk' -import { formatUnits } from 'viem' +import { formatUnits, type Hash } from 'viem' import { getClientAddress, isSessionKeyMode } from '../synapse/index.js' import { assertPriceNonZero } from '../utils/validate-pricing.js' import { @@ -289,7 +289,7 @@ export async function depositUSDFC( const amountMoreThanCurrentAllowance = (await synapse.payments.allowance({ spender: synapse.chain.contracts.filecoinPay.address })) < amount - let txHash: `0x${string}` + let txHash: Hash if (amountMoreThanCurrentAllowance || needsAllowanceUpdate) { txHash = await synapse.payments.depositWithPermitAndApproveOperator({ diff --git a/src/core/piece/remove-piece.ts b/src/core/piece/remove-piece.ts index e0dfa201..da13a697 100644 --- a/src/core/piece/remove-piece.ts +++ b/src/core/piece/remove-piece.ts @@ -1,4 +1,5 @@ import type { Synapse } from '@filoz/synapse-sdk' +import type { Hash } from 'viem' import type { StorageContext } from '@filoz/synapse-sdk/storage' import type { Logger } from 'pino' import { getErrorMessage } from '../utils/errors.js' @@ -18,13 +19,13 @@ import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' */ export type RemovePieceProgressEvents = | ProgressEvent<'remove-piece:submitting', { pieceCid: string; dataSetId: bigint }> - | ProgressEvent<'remove-piece:submitted', { pieceCid: string; dataSetId: bigint; txHash: `0x${string}` }> - | ProgressEvent<'remove-piece:confirming', { pieceCid: string; dataSetId: bigint; txHash: `0x${string}` }> + | ProgressEvent<'remove-piece:submitted', { pieceCid: string; dataSetId: bigint; txHash: Hash }> + | ProgressEvent<'remove-piece:confirming', { pieceCid: string; dataSetId: bigint; txHash: Hash }> | ProgressEvent< 'remove-piece:confirmation-failed', - { pieceCid: string; dataSetId: bigint; txHash: `0x${string}`; message: string } + { pieceCid: string; dataSetId: bigint; txHash: Hash; message: string } > - | ProgressEvent<'remove-piece:complete', { txHash: `0x${string}`; confirmed: boolean }> + | ProgressEvent<'remove-piece:complete', { txHash: Hash; confirmed: boolean }> /** * Number of block confirmations to wait for when waitForConfirmation=true @@ -79,7 +80,7 @@ export async function removePiece( pieceCid: string, storageContext: StorageContext, options: RemovePieceOptions -): Promise<`0x${string}`> { +): Promise { const { onProgress, waitForConfirmation } = options const dataSetId = storageContext.dataSetId diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index 1f37293a..88915b0c 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -15,7 +15,7 @@ export { calibration, mainnet, type Chain } import type { SessionKey } from '@filoz/synapse-core/session-key' import { fromSecp256k1 } from '@filoz/synapse-core/session-key' import type { Logger } from 'pino' -import { type Account, custom, getAddress, type HttpTransport, http, type WebSocketTransport, webSocket } from 'viem' +import { type Account, type Address, custom, getAddress, type Hex, type HttpTransport, http, type WebSocketTransport, webSocket } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { APPLICATION_SOURCE } from './constants.js' @@ -57,22 +57,22 @@ interface BaseSynapseConfig { * Standard authentication with private key */ export interface PrivateKeyConfig extends BaseSynapseConfig { - privateKey: `0x${string}` + privateKey: Hex } /** * Session key authentication with owner address and session key private key */ export interface SessionKeyConfig extends BaseSynapseConfig { - walletAddress: `0x${string}` - sessionKey: `0x${string}` + walletAddress: Address + sessionKey: Hex } /** * Read-only mode using an address (cannot sign transactions) */ export interface ReadOnlyConfig extends BaseSynapseConfig { - walletAddress: `0x${string}` + walletAddress: Address readOnly: true } @@ -131,7 +131,7 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log const rpcUrl = config.rpcUrl ?? chain.rpcUrls.default.webSocket?.[0] ?? chain.rpcUrls.default.http[0] const transport = rpcUrl ? createTransport(rpcUrl) : undefined - let account: Account | `0x${string}` + let account: Account | Address let sessionKey: SessionKey<'Secp256k1'> | undefined if (isReadOnlyConfig(config)) { @@ -198,9 +198,9 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log * Handles both string addresses (read-only / session key mode) and * full Account objects (private key mode). */ -export function getClientAddress(synapse: Synapse): `0x${string}` { +export function getClientAddress(synapse: Synapse): Address { const account = synapse.client.account - return (typeof account === 'string' ? account : account.address) as `0x${string}` + return (typeof account === 'string' ? account : account.address) as Address } /** diff --git a/src/core/upload/synapse.ts b/src/core/upload/synapse.ts index 7cc27413..79608657 100644 --- a/src/core/upload/synapse.ts +++ b/src/core/upload/synapse.ts @@ -9,13 +9,14 @@ import type { CopyResult, FailedAttempt, PieceCID, PullStatus, Synapse, UploadRe import { METADATA_KEYS, type PDPProvider } from '@filoz/synapse-sdk' import type { StorageManagerUploadOptions } from '@filoz/synapse-sdk/storage' import type { CID } from 'multiformats/cid' +import type { Hash } from 'viem' import type { Logger } from 'pino' import { DEFAULT_DATA_SET_METADATA } from '../synapse/constants.js' import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' export type UploadProgressEvents = | ProgressEvent<'onStored', { providerId: bigint; pieceCid: PieceCID }> - | ProgressEvent<'onPiecesAdded', { txHash: `0x${string}`; providerId: bigint }> + | ProgressEvent<'onPiecesAdded', { txHash: Hash; providerId: bigint }> | ProgressEvent<'onPiecesConfirmed', { dataSetId: bigint; providerId: bigint; pieceIds: bigint[] }> | ProgressEvent<'onCopyComplete', { providerId: bigint; pieceCid: PieceCID }> | ProgressEvent<'onCopyFailed', { providerId: bigint; pieceCid: PieceCID; error: Error }> diff --git a/src/filecoin-pinning-server.ts b/src/filecoin-pinning-server.ts index 4db193c8..bfc89d18 100644 --- a/src/filecoin-pinning-server.ts +++ b/src/filecoin-pinning-server.ts @@ -1,7 +1,7 @@ import fastify, { type FastifyInstance, type FastifyRequest } from 'fastify' import { CID } from 'multiformats/cid' import type { Logger } from 'pino' -import type { Address } from 'viem' +import type { Address, Hex } from 'viem' import type { Config } from './core/synapse/index.js' import { initializeSynapse, type SynapseSetupConfig } from './core/synapse/index.js' import { FilecoinPinStore, type PinOptions } from './filecoin-pin-store.js' @@ -28,12 +28,12 @@ function buildSynapseConfig(config: Config): SynapseSetupConfig { return { ...base, walletAddress: config.walletAddress as Address, - sessionKey: config.sessionKey as Address, + sessionKey: config.sessionKey as Hex, } } if (config.privateKey) { - return { ...base, privateKey: config.privateKey as Address } + return { ...base, privateKey: config.privateKey as Hex } } throw new Error( diff --git a/src/payments/interactive.ts b/src/payments/interactive.ts index f23ed8e9..d24d8b5b 100644 --- a/src/payments/interactive.ts +++ b/src/payments/interactive.ts @@ -9,7 +9,7 @@ import { cancel, confirm, isCancel, password, text } from '@clack/prompts' import { calibration, mainnet } from '@filoz/synapse-sdk' import pc from 'picocolors' -import { parseUnits } from 'viem' +import { parseUnits, type Hex } from 'viem' import { calculateDepositCapacity, checkAndSetAllowances, @@ -84,7 +84,7 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise const rpcUrl = options.rpcUrl || defaultRpcUrl const config: PrivateKeyConfig = { - privateKey: privateKey as `0x${string}`, + privateKey: privateKey as Hex, } if (rpcUrl) { config.rpcUrl = rpcUrl diff --git a/src/test/unit/remove-piece.test.ts b/src/test/unit/remove-piece.test.ts index 52ef167d..0a71a9cc 100644 --- a/src/test/unit/remove-piece.test.ts +++ b/src/test/unit/remove-piece.test.ts @@ -1,9 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Hash } from 'viem' import { removePiece } from '../../core/piece/index.js' const { mockDeletePiece, mockWaitForTransactionReceipt, mockSynapse, storageContext, state } = vi.hoisted(() => { const state = { - txHash: '0xtest-hash' as `0x${string}`, + txHash: '0x7e27000000000000000000000000000000000000000000000000000000000000' as Hash, dataSetId: 99n, } @@ -26,7 +27,7 @@ const { mockDeletePiece, mockWaitForTransactionReceipt, mockSynapse, storageCont describe('removePiece', () => { beforeEach(() => { vi.clearAllMocks() - state.txHash = '0xtest-hash' + state.txHash = '0x7e27000000000000000000000000000000000000000000000000000000000000' state.dataSetId = 99n storageContext.dataSetId = state.dataSetId }) From adfa40b5aa92483d9b5c3ba590becb35afe3aa95 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 25 Mar 2026 15:24:59 -0500 Subject: [PATCH 03/10] docs: update auth docs to reflect private key and session key options Co-Authored-By: Claude Sonnet 4.6 --- README.md | 13 ++++++++++--- src/config.ts | 14 +++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f52fdff8..14b66cda 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Use Filecoin Pin programmatically in your Node.js or browser applications. The l Run a localhost IPFS Pinning Service API server that implements the [IPFS Pinning Service API specification](https://ipfs.github.io/pinning-services-api-spec/). This allows you to use standard IPFS tooling (like `ipfs pin remote`) while storing data on Filecoin. - **Repository**: This repo (`filecoin-pin server` command in CLI) -- **Usage**: `PRIVATE_KEY=0x... npx filecoin-pin server` +- **Usage**: `PRIVATE_KEY=0x... npx filecoin-pin server` (or use session key auth — see [Configuration](#configuration)) - **Status**: Works and is tested, but hasn't received as many features as the CLI. If it would benefit your usecase, please comment on [tracking issue](https://github.com/filecoin-project/filecoin-pin/issues/46) so we can be better informed when it comes to prioritizing. ### Management Console GUI @@ -139,7 +139,11 @@ npm install -g filecoin-pin ### Basic Usage ```bash -# 0. Set your PRIVATE_KEY environment variable or pass it via --private-key to each command. +# 0. Set up authentication (choose one): +# Private key: export PRIVATE_KEY=0x... +# (or pass --private-key to each command) +# Session key: export WALLET_ADDRESS=0x... SESSION_KEY=0x... +# (or pass --wallet-address --session-key to each command) # 1. Configure payment permissions (one-time setup) filecoin-pin payments setup --auto @@ -221,7 +225,10 @@ When using `--network devnet`, Filecoin Pin reads connection details from a runn * `-h`, `--help`: Display help information for each command * `-V`, `--version`: Output the version number * `-v`, `--verbose`: Verbose output -* `--private-key`: Ethereum-style (`0x`) private key, funded with USDFC (required) +* `--private-key`: Ethereum-style (`0x`) private key (wallet and signer), funded with USDFC +* `--wallet-address`: Session key mode: owner wallet address +* `--session-key`: Session key mode: scoped signing key registered to the wallet +* `--view-address`: Read-only address for querying storage status without signing * `--network`: Filecoin network to use: `mainnet`, `calibration`, or `devnet` (default: `calibration`) * `--rpc-url`: Filecoin RPC endpoint (overrides `--network` if specified) diff --git a/src/config.ts b/src/config.ts index 6d35dfe3..eb944aa7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,10 +29,14 @@ function getDataDirectory(): string { /** * Create configuration from environment variables * - * This demonstrates configuration best practices for Synapse SDK: - * - PRIVATE_KEY: Required for transaction signing (keep secure!) - * - RPC_URL: Filecoin network endpoint (mainnet or calibration) - takes precedence over NETWORK - * - NETWORK: Filecoin network name (mainnet or calibration) - used if RPC_URL not set + * Authentication (choose one): + * - PRIVATE_KEY: Standard private key auth + * - WALLET_ADDRESS + SESSION_KEY: Session key auth + * - VIEW_ADDRESS: Read-only mode, no signing (CLI only) + * + * Network: + * - RPC_URL: Filecoin RPC endpoint — takes precedence over NETWORK + * - NETWORK: Filecoin network name (mainnet, calibration, devnet) — used if RPC_URL not set */ export function createConfig(): Config { const dataDir = getDataDirectory() @@ -49,7 +53,7 @@ export function createConfig(): Config { host: process.env.HOST ?? 'localhost', // Synapse SDK configuration - privateKey: process.env.PRIVATE_KEY, // Required: Ethereum-compatible private key + privateKey: process.env.PRIVATE_KEY, walletAddress: process.env.WALLET_ADDRESS, sessionKey: process.env.SESSION_KEY, viewAddress: process.env.VIEW_ADDRESS, From 1aa9b9e5cc60b81e657a790dcfb70f339164b061 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 25 Mar 2026 15:39:27 -0500 Subject: [PATCH 04/10] test(server): add coverage for session-key auth and no-auth error paths Co-Authored-By: Claude Sonnet 4.6 --- src/test/mocks/synapse-core-session-key.ts | 11 ++ .../unit/filecoin-pinning-server.unit.test.ts | 102 ++++++++++++++++++ src/test/unit/synapse-service.test.ts | 29 +++++ 3 files changed, 142 insertions(+) create mode 100644 src/test/mocks/synapse-core-session-key.ts create mode 100644 src/test/unit/filecoin-pinning-server.unit.test.ts diff --git a/src/test/mocks/synapse-core-session-key.ts b/src/test/mocks/synapse-core-session-key.ts new file mode 100644 index 00000000..8a8ceb24 --- /dev/null +++ b/src/test/mocks/synapse-core-session-key.ts @@ -0,0 +1,11 @@ +import { vi } from 'vitest' + +/** + * Mock implementation of @filoz/synapse-core/session-key for testing + * + * fromSecp256k1 returns a minimal SessionKey-shaped object with a no-op + * syncExpirations so tests never hit the real network. + */ +export const fromSecp256k1 = vi.fn(() => ({ + syncExpirations: vi.fn().mockResolvedValue(undefined), +})) diff --git a/src/test/unit/filecoin-pinning-server.unit.test.ts b/src/test/unit/filecoin-pinning-server.unit.test.ts new file mode 100644 index 00000000..ae38d6e3 --- /dev/null +++ b/src/test/unit/filecoin-pinning-server.unit.test.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createConfig } from '../../config.js' +import { createFilecoinPinningServer } from '../../filecoin-pinning-server.js' +import { createLogger } from '../../logger.js' + +vi.mock('@filoz/synapse-sdk', async (importOriginal) => { + const actual = await importOriginal() + const mockModule = await import('../mocks/synapse-sdk.js') + return { ...mockModule, SIZE_CONSTANTS: actual.SIZE_CONSTANTS } +}) + +vi.mock('@filoz/synapse-core/session-key', async () => await import('../mocks/synapse-core-session-key.js')) + +const SERVICE_INFO = { service: 'filecoin-pin', version: '0.1.0' } +const TEST_OUTPUT_DIR = './test-unit-pinning-server-cars' + +describe('createFilecoinPinningServer auth selection', () => { + let server: any + let pinStore: any + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(async () => { + if (server != null) { + await server.close() + server = undefined + } + if (pinStore != null) { + await pinStore.stop() + pinStore = undefined + } + }) + + it('should start successfully with session key auth (walletAddress + sessionKey)', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', + } + const logger = createLogger(config) + + const result = await createFilecoinPinningServer(config, logger, SERVICE_INFO) + server = result.server + pinStore = result.pinStore + + expect(server).toBeDefined() + expect(pinStore).toBeDefined() + }) + + it('should throw "No authentication configured" when neither privateKey nor sessionKey provided', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'No authentication configured' + ) + }) + + it('should include --private-key and --session-key hints in the no-auth error message', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + /--private-key.*PRIVATE_KEY|--wallet-address.*--session-key/ + ) + }) + + it('should throw when walletAddress is set but sessionKey is missing', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'No authentication configured' + ) + }) +}) diff --git a/src/test/unit/synapse-service.test.ts b/src/test/unit/synapse-service.test.ts index ddcbe50e..abfd4e30 100644 --- a/src/test/unit/synapse-service.test.ts +++ b/src/test/unit/synapse-service.test.ts @@ -9,6 +9,9 @@ import { MockSynapse } from '../mocks/synapse-mocks.js' // Mock the Synapse SDK vi.mock('@filoz/synapse-sdk', async () => await import('../mocks/synapse-sdk.js')) +// Mock the session key module so tests never hit the real network +vi.mock('@filoz/synapse-core/session-key', async () => await import('../mocks/synapse-core-session-key.js')) + // Test CID for upload tests const TEST_CID = CID.parse('bafkreia5fn4rmshmb7cl7fufkpcw733b5anhuhydtqstnglpkzosqln5kq') @@ -71,6 +74,32 @@ describe('synapse-service', () => { 'Initializing Synapse (read-only)' ) }) + + it('should initialize Synapse in session-key mode', async () => { + const config: SynapseSetupConfig = { + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', + rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', + } + + const infoSpy = vi.spyOn(logger, 'info') + const synapse = await initializeSynapse(config, logger) + + expect(synapse).toBeDefined() + expect(infoSpy).toHaveBeenCalledWith( + expect.objectContaining({ event: 'synapse.init', mode: 'session-key' }), + 'Initializing Synapse (session key)' + ) + }) + + it('should throw when no authentication is provided', async () => { + // AccountConfig with null account satisfies the type but triggers the no-auth branch + const config = { rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1' } as any + + await expect(initializeSynapse(config, logger)).rejects.toThrow( + 'No authentication provided' + ) + }) }) describe('uploadToSynapse', () => { From 5ca16337296b2a398d7c6c8818062bffa89a6ec8 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 25 Mar 2026 15:44:10 -0500 Subject: [PATCH 05/10] style: fix biome lint and format errors Co-Authored-By: Claude Sonnet 4.6 --- src/core/piece/remove-piece.ts | 2 +- src/core/synapse/index.ts | 12 +++++++++++- src/core/upload/synapse.ts | 2 +- src/payments/interactive.ts | 2 +- src/test/mocks/synapse-core-session-key.ts | 4 ++-- src/test/unit/remove-piece.test.ts | 2 +- src/test/unit/synapse-service.test.ts | 4 +--- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/core/piece/remove-piece.ts b/src/core/piece/remove-piece.ts index da13a697..8425b40e 100644 --- a/src/core/piece/remove-piece.ts +++ b/src/core/piece/remove-piece.ts @@ -1,7 +1,7 @@ import type { Synapse } from '@filoz/synapse-sdk' -import type { Hash } from 'viem' import type { StorageContext } from '@filoz/synapse-sdk/storage' import type { Logger } from 'pino' +import type { Hash } from 'viem' import { getErrorMessage } from '../utils/errors.js' import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index 88915b0c..b82951bd 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -15,7 +15,17 @@ export { calibration, mainnet, type Chain } import type { SessionKey } from '@filoz/synapse-core/session-key' import { fromSecp256k1 } from '@filoz/synapse-core/session-key' import type { Logger } from 'pino' -import { type Account, type Address, custom, getAddress, type Hex, type HttpTransport, http, type WebSocketTransport, webSocket } from 'viem' +import { + type Account, + type Address, + custom, + getAddress, + type Hex, + type HttpTransport, + http, + type WebSocketTransport, + webSocket, +} from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { APPLICATION_SOURCE } from './constants.js' diff --git a/src/core/upload/synapse.ts b/src/core/upload/synapse.ts index 79608657..5a2cd515 100644 --- a/src/core/upload/synapse.ts +++ b/src/core/upload/synapse.ts @@ -9,8 +9,8 @@ import type { CopyResult, FailedAttempt, PieceCID, PullStatus, Synapse, UploadRe import { METADATA_KEYS, type PDPProvider } from '@filoz/synapse-sdk' import type { StorageManagerUploadOptions } from '@filoz/synapse-sdk/storage' import type { CID } from 'multiformats/cid' -import type { Hash } from 'viem' import type { Logger } from 'pino' +import type { Hash } from 'viem' import { DEFAULT_DATA_SET_METADATA } from '../synapse/constants.js' import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' diff --git a/src/payments/interactive.ts b/src/payments/interactive.ts index d24d8b5b..e158b90c 100644 --- a/src/payments/interactive.ts +++ b/src/payments/interactive.ts @@ -9,7 +9,7 @@ import { cancel, confirm, isCancel, password, text } from '@clack/prompts' import { calibration, mainnet } from '@filoz/synapse-sdk' import pc from 'picocolors' -import { parseUnits, type Hex } from 'viem' +import { type Hex, parseUnits } from 'viem' import { calculateDepositCapacity, checkAndSetAllowances, diff --git a/src/test/mocks/synapse-core-session-key.ts b/src/test/mocks/synapse-core-session-key.ts index 8a8ceb24..30846d4d 100644 --- a/src/test/mocks/synapse-core-session-key.ts +++ b/src/test/mocks/synapse-core-session-key.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { type Mock, vi } from 'vitest' /** * Mock implementation of @filoz/synapse-core/session-key for testing @@ -6,6 +6,6 @@ import { vi } from 'vitest' * fromSecp256k1 returns a minimal SessionKey-shaped object with a no-op * syncExpirations so tests never hit the real network. */ -export const fromSecp256k1 = vi.fn(() => ({ +export const fromSecp256k1: Mock = vi.fn(() => ({ syncExpirations: vi.fn().mockResolvedValue(undefined), })) diff --git a/src/test/unit/remove-piece.test.ts b/src/test/unit/remove-piece.test.ts index 0a71a9cc..530c536b 100644 --- a/src/test/unit/remove-piece.test.ts +++ b/src/test/unit/remove-piece.test.ts @@ -1,5 +1,5 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Hash } from 'viem' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { removePiece } from '../../core/piece/index.js' const { mockDeletePiece, mockWaitForTransactionReceipt, mockSynapse, storageContext, state } = vi.hoisted(() => { diff --git a/src/test/unit/synapse-service.test.ts b/src/test/unit/synapse-service.test.ts index abfd4e30..11bcab0c 100644 --- a/src/test/unit/synapse-service.test.ts +++ b/src/test/unit/synapse-service.test.ts @@ -96,9 +96,7 @@ describe('synapse-service', () => { // AccountConfig with null account satisfies the type but triggers the no-auth branch const config = { rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1' } as any - await expect(initializeSynapse(config, logger)).rejects.toThrow( - 'No authentication provided' - ) + await expect(initializeSynapse(config, logger)).rejects.toThrow('No authentication provided') }) }) From 7889729b33754704a1c5520d6eb7b41c8911c777 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Mon, 30 Mar 2026 12:39:24 -0500 Subject: [PATCH 06/10] separate error for wallet address without session key and session key without wallet address --- src/core/synapse/index.ts | 14 ++++++++++++++ src/test/unit/synapse-service.test.ts | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index b82951bd..4eaf26ec 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -165,6 +165,20 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log account = config.account logger?.info({ event: 'synapse.init', mode: 'account' }, 'Initializing Synapse (pre-created account)') } else { + const hasWallet = 'walletAddress' in config && config.walletAddress != null + const hasSessionKey = 'sessionKey' in config && config.sessionKey != null + if (hasWallet && !hasSessionKey) { + throw new Error( + 'Session key authentication requires both --wallet-address and --session-key. ' + + 'Missing: --session-key / SESSION_KEY.' + ) + } + if (hasSessionKey && !hasWallet) { + throw new Error( + 'Session key authentication requires both --wallet-address and --session-key. ' + + 'Missing: --wallet-address / WALLET_ADDRESS.' + ) + } throw new Error( 'No authentication provided. Supply a private key (--private-key / PRIVATE_KEY), ' + 'wallet address (--wallet-address / WALLET_ADDRESS), or session key (--session-key / SESSION_KEY).' diff --git a/src/test/unit/synapse-service.test.ts b/src/test/unit/synapse-service.test.ts index 11bcab0c..f6bc180d 100644 --- a/src/test/unit/synapse-service.test.ts +++ b/src/test/unit/synapse-service.test.ts @@ -98,6 +98,20 @@ describe('synapse-service', () => { await expect(initializeSynapse(config, logger)).rejects.toThrow('No authentication provided') }) + + it('should throw when walletAddress is provided without sessionKey', async () => { + const config = { walletAddress: '0x1234567890123456789012345678901234567890' } as any + + await expect(initializeSynapse(config, logger)).rejects.toThrow('Missing: --session-key / SESSION_KEY') + }) + + it('should throw when sessionKey is provided without walletAddress', async () => { + const config = { + sessionKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + } as any + + await expect(initializeSynapse(config, logger)).rejects.toThrow('Missing: --wallet-address / WALLET_ADDRESS') + }) }) describe('uploadToSynapse', () => { From 3e3d56c4a190a50d5d7548afba2ecc1a340855d5 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Mon, 30 Mar 2026 13:44:21 -0500 Subject: [PATCH 07/10] check isHex, isAddress, and private key length --- src/filecoin-pinning-server.ts | 23 +++++- .../unit/filecoin-pinning-server.unit.test.ts | 80 +++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/filecoin-pinning-server.ts b/src/filecoin-pinning-server.ts index bfc89d18..39fbab7f 100644 --- a/src/filecoin-pinning-server.ts +++ b/src/filecoin-pinning-server.ts @@ -1,7 +1,7 @@ import fastify, { type FastifyInstance, type FastifyRequest } from 'fastify' import { CID } from 'multiformats/cid' import type { Logger } from 'pino' -import type { Address, Hex } from 'viem' +import { isAddress, isHex } from 'viem' import type { Config } from './core/synapse/index.js' import { initializeSynapse, type SynapseSetupConfig } from './core/synapse/index.js' import { FilecoinPinStore, type PinOptions } from './filecoin-pin-store.js' @@ -25,15 +25,30 @@ function buildSynapseConfig(config: Config): SynapseSetupConfig { const base = { rpcUrl: config.rpcUrl } if (config.walletAddress && config.sessionKey) { + if (!isAddress(config.walletAddress)) { + throw new Error('Wallet address must be an ethereum address') + } + if (!isHex(config.sessionKey)) { + throw new Error('Session key must be 0x-prefixed hexadecimal') + } + if (config.sessionKey.length !== 66) { + throw new Error('Session key must be 32 bytes') + } return { ...base, - walletAddress: config.walletAddress as Address, - sessionKey: config.sessionKey as Hex, + walletAddress: config.walletAddress, + sessionKey: config.sessionKey, } } if (config.privateKey) { - return { ...base, privateKey: config.privateKey as Hex } + if (!isHex(config.privateKey)) { + throw new Error('Private key must be 0x-prefixed hexadecimal') + } + if (config.privateKey.length !== 66) { + throw new Error('Private key must be 32 bytes') + } + return { ...base, privateKey: config.privateKey } } throw new Error( diff --git a/src/test/unit/filecoin-pinning-server.unit.test.ts b/src/test/unit/filecoin-pinning-server.unit.test.ts index ae38d6e3..1ae1d4e5 100644 --- a/src/test/unit/filecoin-pinning-server.unit.test.ts +++ b/src/test/unit/filecoin-pinning-server.unit.test.ts @@ -99,4 +99,84 @@ describe('createFilecoinPinningServer auth selection', () => { 'No authentication configured' ) }) + + it('should throw when walletAddress is not a valid ethereum address', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: 'not-an-address', + sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Wallet address must be an ethereum address' + ) + }) + + it('should throw when sessionKey is not 0x-prefixed hex', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: 'not-hex-at-all', + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Session key must be 0x-prefixed hexadecimal' + ) + }) + + it('should throw when sessionKey is valid hex but not 32 bytes', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: '0x1234', + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Session key must be 32 bytes' + ) + }) + + it('should throw when privateKey is not valid hex', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: 'not-a-hex-key', + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Private key must be 0x-prefixed hexadecimal' + ) + }) + + it('should throw when privateKey is valid hex but not 32 bytes', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: '0x1234', + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Private key must be 32 bytes' + ) + }) }) From f0ee931d5f2822c2c68ecf31d2fc71e97c64aec8 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Wed, 25 Mar 2026 17:59:56 -0500 Subject: [PATCH 08/10] test(mocks): add missing session-key exports to synapse-core mock Co-Authored-By: Claude Sonnet 4.6 --- src/test/mocks/synapse-core-session-key.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/test/mocks/synapse-core-session-key.ts b/src/test/mocks/synapse-core-session-key.ts index 30846d4d..d3cd0a90 100644 --- a/src/test/mocks/synapse-core-session-key.ts +++ b/src/test/mocks/synapse-core-session-key.ts @@ -3,9 +3,21 @@ import { type Mock, vi } from 'vitest' /** * Mock implementation of @filoz/synapse-core/session-key for testing * - * fromSecp256k1 returns a minimal SessionKey-shaped object with a no-op - * syncExpirations so tests never hit the real network. + * Exports real permission constants (hardcoded to avoid circular mock imports) + * and overrides fromSecp256k1 so tests never hit the real network. */ + +export const CreateDataSetPermission = '0x25ebf20299107c91b4624d5bac3a16d32cabf0db23b450ee09ab7732983b1dc9' +export const AddPiecesPermission = '0x954bdc254591a7eab1b73f03842464d9283a08352772737094d710a4428fd183' +export const SchedulePieceRemovalsPermission = '0x5415701e313bb627e755b16924727217bb356574fe20e7061442c200b0822b22' + +export const DefaultFwssPermissions = [CreateDataSetPermission, AddPiecesPermission, SchedulePieceRemovalsPermission] + +const mockExpirations = Object.fromEntries(DefaultFwssPermissions.map((p) => [p, 0n])) + export const fromSecp256k1: Mock = vi.fn(() => ({ syncExpirations: vi.fn().mockResolvedValue(undefined), + hasPermission: vi.fn().mockReturnValue(true), + expirations: mockExpirations, + address: '0x0000000000000000000000000000000000000001', })) From f05a120cc5373d6c4b9a19233fa4cb530e04db03 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 3 Apr 2026 04:06:27 -0500 Subject: [PATCH 09/10] no view address for server mode --- README.md | 1 - src/config.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/README.md b/README.md index 14b66cda..1a7c64dc 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,6 @@ When using `--network devnet`, Filecoin Pin reads connection details from a runn * `--private-key`: Ethereum-style (`0x`) private key (wallet and signer), funded with USDFC * `--wallet-address`: Session key mode: owner wallet address * `--session-key`: Session key mode: scoped signing key registered to the wallet -* `--view-address`: Read-only address for querying storage status without signing * `--network`: Filecoin network to use: `mainnet`, `calibration`, or `devnet` (default: `calibration`) * `--rpc-url`: Filecoin RPC endpoint (overrides `--network` if specified) diff --git a/src/config.ts b/src/config.ts index ff5b75f9..5fcd85d2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,7 +32,6 @@ function getDataDirectory(): string { * Authentication (choose one): * - PRIVATE_KEY: Standard private key auth * - WALLET_ADDRESS + SESSION_KEY: Session key auth - * - VIEW_ADDRESS: Read-only mode, no signing (CLI only) * * Network: * - RPC_URL: Filecoin RPC endpoint — takes precedence over NETWORK From fa5c66466ad43cfb3bbe335ae3cb3be47c55b61f Mon Sep 17 00:00:00 2001 From: William Morriss Date: Fri, 3 Apr 2026 04:17:12 -0500 Subject: [PATCH 10/10] doc: add string to name the fictional delete dataset permission --- src/core/synapse/index.ts | 2 ++ src/test/mocks/synapse-core-session-key.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index bab09e50..4e86d5fa 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -17,6 +17,7 @@ import { AddPiecesPermission, CreateDataSetPermission, DefaultFwssPermissions, + DeleteDataSetPermission, fromSecp256k1, SchedulePieceRemovalsPermission, } from '@filoz/synapse-core/session-key' @@ -137,6 +138,7 @@ function createTransport(rpcUrl: string): HttpTransport | WebSocketTransport { const PERMISSION_NAMES: Record = { [CreateDataSetPermission]: 'CreateDataSet', + [DeleteDataSetPermission]: 'DeleteDataSet', [AddPiecesPermission]: 'AddPieces', [SchedulePieceRemovalsPermission]: 'SchedulePieceRemovals', } diff --git a/src/test/mocks/synapse-core-session-key.ts b/src/test/mocks/synapse-core-session-key.ts index d3cd0a90..eeba5823 100644 --- a/src/test/mocks/synapse-core-session-key.ts +++ b/src/test/mocks/synapse-core-session-key.ts @@ -8,10 +8,16 @@ import { type Mock, vi } from 'vitest' */ export const CreateDataSetPermission = '0x25ebf20299107c91b4624d5bac3a16d32cabf0db23b450ee09ab7732983b1dc9' +export const DeleteDataSetPermission = '0xb5d6b3fc97881f05e96958136ac09d7e0bc7cbf17ea92fce7c431d88132d2b58' export const AddPiecesPermission = '0x954bdc254591a7eab1b73f03842464d9283a08352772737094d710a4428fd183' export const SchedulePieceRemovalsPermission = '0x5415701e313bb627e755b16924727217bb356574fe20e7061442c200b0822b22' -export const DefaultFwssPermissions = [CreateDataSetPermission, AddPiecesPermission, SchedulePieceRemovalsPermission] +export const DefaultFwssPermissions = [ + CreateDataSetPermission, + DeleteDataSetPermission, + AddPiecesPermission, + SchedulePieceRemovalsPermission, +] const mockExpirations = Object.fromEntries(DefaultFwssPermissions.map((p) => [p, 0n]))