You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@resilientdb.apache.org by bi...@apache.org on 2026/04/11 08:14:59 UTC

(incubator-resilientdb) branch development updated: Add ResShare file-sharing toolkit under ecosystem/tools (#240)

This is an automated email from the ASF dual-hosted git repository.

bisman pushed a commit to branch development
in repository https://gitbox.apache.org/repos/asf/incubator-resilientdb.git


The following commit(s) were added to refs/heads/development by this push:
     new 1dfa2c03 Add ResShare file-sharing toolkit under ecosystem/tools (#240)
1dfa2c03 is described below

commit 1dfa2c0338feef5da80224b4cfc3eeedf9538d20
Author: Devang Borkar <de...@gmail.com>
AuthorDate: Sat Apr 11 01:14:54 2026 -0700

    Add ResShare file-sharing toolkit under ecosystem/tools (#240)
    
    * Fix covery test (#246)
    
    * fix ut
    
    * fix ut
    
    * Add implementation of ResShare Toolkit
    
    - Created core files including `README.md`, `package.json`, and configuration for Jest testing.
    - Implemented main client functionality in `src/index.js` and modularized authentication, file handling, and sharing features in respective modules.
    - Added example scripts for basic usage and advanced scenarios.
    - Included a `.gitignore` file to exclude unnecessary files from version control.
    - Established a testing framework with initial unit tests for client functionality.
    
    * Document ResShare SDK in READMEs
    
    * Add Apache headers to ResShare toolkit files
    
    ---------
    
    Co-authored-by: cjcchen <ic...@gmail.com>
---
 .bazelversion                                      |   1 -
 README.md                                          |   4 +-
 ecosystem/README.md                                |   1 +
 ecosystem/tools/reshare-lib/.gitignore             |  21 ++
 ecosystem/tools/reshare-lib/README.md              | 162 +++++++++++
 .../tools/reshare-lib/examples/advanced-demo.js    | 256 +++++++++++++++++
 .../tools/reshare-lib/examples/basic-usage.js      |  41 +++
 ecosystem/tools/reshare-lib/jest.config.cjs        |  24 ++
 ecosystem/tools/reshare-lib/package.json           |  42 +++
 ecosystem/tools/reshare-lib/src/index.js           | 304 +++++++++++++++++++++
 ecosystem/tools/reshare-lib/src/modules/auth.js    |  61 +++++
 ecosystem/tools/reshare-lib/src/modules/files.js   |  87 ++++++
 ecosystem/tools/reshare-lib/src/modules/shares.js  |  68 +++++
 .../tools/reshare-lib/tests/unit/client.test.js    | 164 +++++++++++
 platform/consensus/recovery/recovery.cpp           |   8 +-
 platform/consensus/recovery/recovery_test.cpp      |   5 +-
 16 files changed, 1240 insertions(+), 9 deletions(-)

diff --git a/.bazelversion b/.bazelversion
index 4df4a2ec..78989021 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1,3 +1,2 @@
 6.0.0
 
-
diff --git a/README.md b/README.md
index efd5f60d..828fea00 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,7 @@ Beyond the core NexRes engine, ResilientDB ships with a rich ecosystem of tools
 - **Core Node (NexRes)**: High-throughput, BFT-replicated state machine that orders and executes transactions with strong consistency and durability.
 - **APIs (GraphQL & REST)**: ResilientDB exposes HTTP and GraphQL services for committing and querying transactions, making it easy to integrate with web backends, data services, and AI workflows.
 - **ResVault (Wallet & Extension)**: Browser extension wallet that manages keys, lets users authenticate, and approve transactions from web apps, tying writes on-chain to user-controlled identities.
-- **SDKs & ORMs**: Language-specific libraries (e.g., Python ORM, Rust SDK, ResVault SDK) that provide higher-level APIs for modeling assets, talking to GraphQL/REST, and wiring ResVault into applications.
+- **SDKs & ORMs**: Language-specific libraries and client toolkits (e.g., Python ORM, Rust SDK, ResVault SDK, ResShare SDK) that provide higher-level APIs for modeling assets, talking to GraphQL/REST, and wiring ResilientDB services into applications.
 - **Monitoring (ResLens & dashboards)**: Monitoring UIs and middleware (ResLens stack) that surface cluster health, performance metrics, and profiling data for running deployments.
 - **Deployment Tooling (Orbit, Ansible, Docker)**: Tools and playbooks for bringing up ResilientDB clusters and supporting services (GraphQL, Nginx, etc.) on local and cloud environments using Docker, Ansible, and the Orbit desktop deployer.
 
@@ -144,6 +144,7 @@ incubator-resilientdb/
 │   └── tools/                       # Dev and app tools
 │       ├── create-resilient-app/    # App scaffolding CLI
 │       ├── drawing-lib/             # Shared drawing widget
+│       ├── reshare-lib/             # ResShare SDK
 │       └── resvault/                # ResVault wallet extension
 ├── executor/                        # Transaction execution engine
 │   ├── common/                      # Common execution utilities
@@ -537,4 +538,3 @@ ResilientDB includes a built‑in smart contract service that lets you deploy an
 In addition, you can use the **Key‑Value interfaces** to query or update associated balances via `get_balance` and `set_balance` commands when needed.
 
 For a full, step‑by‑step tutorial (including example JSON files, commands, and responses), see **[Getting Started with Smart Contract on ResilientDB](https://blog.resilientdb.com/2025/02/14/GettingStartedSmartContract.html)**.
-
diff --git a/ecosystem/README.md b/ecosystem/README.md
index 13167ec3..c73b271b 100644
--- a/ecosystem/README.md
+++ b/ecosystem/README.md
@@ -55,6 +55,7 @@ ecosystem/
 └── tools/
     ├── create-resilient-app/
     ├── drawing-lib/
+    ├── reshare-lib/                # ResShare SDK
     └── resvault/
 ```
 
diff --git a/ecosystem/tools/reshare-lib/.gitignore b/ecosystem/tools/reshare-lib/.gitignore
new file mode 100644
index 00000000..efbe507e
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/.gitignore
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+node_modules/
+coverage/
+examples/out/*
+!examples/out/.gitkeep
diff --git a/ecosystem/tools/reshare-lib/README.md b/ecosystem/tools/reshare-lib/README.md
new file mode 100644
index 00000000..6557bedc
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/README.md
@@ -0,0 +1,162 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# ResShare Toolkit
+
+**License**: Apache-2.0
+
+Universal JavaScript toolkit for ResShare file-sharing APIs. This toolkit provides a modular client for authentication, file and folder operations, and share management.
+
+## Features
+
+- Authentication helpers (`signup`, `login`, `logout`, `status`, `deleteUser`)
+- File operations (`createFolder`, `upload`, `deleteItem`, `download`, `downloadZip`)
+- Share operations (`share`, `list`, `remove`)
+- Cookie-aware sessions for Node.js and browser clients
+- Retry and timeout handling with normalized API errors
+
+## Project Structure
+
+```
+reshare-lib/
+├── src/
+│   ├── index.js
+│   └── modules/
+│       ├── auth.js
+│       ├── files.js
+│       └── shares.js
+├── examples/
+│   ├── basic-usage.js
+│   └── advanced-demo.js
+├── tests/
+│   └── unit/
+├── README.md
+├── jest.config.cjs
+└── package.json
+```
+
+## Installation
+
+This toolkit is part of the Apache ResilientDB repository and is not published as a standalone package yet.
+
+```bash
+# From repository root
+npm install ./ecosystem/tools/reshare-lib
+```
+
+## Quick Start
+
+```javascript
+import ResShareToolkitClient from 'resshare-toolkit';
+
+const client = new ResShareToolkitClient({
+  baseUrl: 'http://localhost:5000'
+});
+
+await client.auth.login({ username: 'alice', password: 'secret' });
+await client.files.createFolder('docs');
+```
+
+## API Overview
+
+### Client Initialization
+
+```javascript
+const client = new ResShareToolkitClient({
+  baseUrl: 'http://localhost:5000',
+  apiPrefix: '',                 // Optional path prefix (for example: /api/v1)
+  timeout: 30000,                // Request timeout in milliseconds
+  retries: 2,                    // Request retries
+  credentials: 'include',        // Fetch credentials mode
+  onAuthExpired: async () => ''  // Optional token refresh callback
+});
+```
+
+### Auth Module
+
+- `client.auth.login({ username, password })`
+- `client.auth.signup({ username, password })`
+- `client.auth.logout()`
+- `client.auth.status()`
+- `client.auth.deleteUser({ password })`
+
+### Files Module
+
+- `client.files.createFolder(path)`
+- `client.files.upload({ file, path, skipProcessing, filename })`
+- `client.files.deleteItem({ path, deleteInRoot })`
+- `client.files.download({ path, isShared })`
+- `client.files.downloadZip({ path, isShared })`
+
+### Shares Module
+
+- `client.shares.share({ target, path, node })`
+- `client.shares.list()`
+- `client.shares.remove({ combinedPath, fromUser, path })`
+
+## Error Handling
+
+All failed requests throw `ApiError` with:
+
+- `status`: HTTP status code
+- `body`: parsed response payload
+
+```javascript
+import ResShareToolkitClient, { ApiError } from 'resshare-toolkit';
+
+const client = new ResShareToolkitClient({ baseUrl: 'http://localhost:5000' });
+
+try {
+  await client.files.createFolder('docs');
+} catch (error) {
+  if (error instanceof ApiError) {
+    console.error(error.status, error.getUserMessage());
+  }
+}
+```
+
+## Examples
+
+```bash
+node ecosystem/tools/reshare-lib/examples/basic-usage.js
+node ecosystem/tools/reshare-lib/examples/advanced-demo.js
+```
+
+`advanced-demo.js` writes outputs to `ecosystem/tools/reshare-lib/examples/out/` during execution.
+
+## Testing
+
+```bash
+cd ecosystem/tools/reshare-lib
+npm test
+```
+
+## Backend Requirements
+
+The toolkit expects a ResShare-compatible backend exposing endpoints used by:
+
+- `/login`, `/signup`, `/logout`, `/auth-status`, `/delete-user`
+- `/create-folder`, `/upload`, `/delete`, `/download`, `/download-zip`
+- `/share`, `/shared`
+
+Configure `baseUrl` and optional `apiPrefix` to match your deployment.
+
+## License
+
+Licensed to the Apache Software Foundation (ASF) under the Apache License, Version 2.0.
diff --git a/ecosystem/tools/reshare-lib/examples/advanced-demo.js b/ecosystem/tools/reshare-lib/examples/advanced-demo.js
new file mode 100644
index 00000000..3f086cb7
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/examples/advanced-demo.js
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import ResShareToolkitClient, { ApiError } from '../src/index.js';
+import { mkdir, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import process from 'node:process';
+
+const OUTPUT_DIR = 'ecosystem/tools/reshare-lib/examples/out';
+
+function nowSlug() {
+  const iso = new Date().toISOString();
+  const compact = iso.replace(/[-:.TZ]/g, '').slice(0, 14);
+  const random = Math.random().toString(16).slice(2, 8);
+  return `${compact}-${random}`;
+}
+
+function getErrorStatus(error) {
+  if (!(error instanceof ApiError)) return null;
+  return error.body?.status || error.body?.result || error.body?.message || null;
+}
+
+async function safeSignup(client, { username, password }) {
+  try {
+    const result = await client.auth.signup({ username, password });
+    console.log(`signup:${username}`, result.status);
+  } catch (error) {
+    const status = getErrorStatus(error);
+    if (status === 'USER_EXISTS') {
+      console.log(`signup:${username}`, status);
+      return;
+    }
+    throw error;
+  }
+}
+
+async function safeCreateFolder(client, folderPath) {
+  try {
+    const result = await client.files.createFolder(folderPath);
+    console.log(`create-folder:${folderPath}`, result.status);
+  } catch (error) {
+    const status = getErrorStatus(error);
+    if (status === 'DUPLICATE_NAME') {
+      console.log(`create-folder:${folderPath}`, status);
+      return;
+    }
+    throw error;
+  }
+}
+
+async function safeShare(client, { target, path: nodePath }) {
+  try {
+    const result = await client.shares.share({ target, path: nodePath });
+    console.log(`share:${nodePath}->${target}`, result.status);
+  } catch (error) {
+    const status = getErrorStatus(error);
+    if (status === 'ALREADY_SHARED') {
+      console.log(`share:${nodePath}->${target}`, status);
+      return;
+    }
+    throw error;
+  }
+}
+
+async function responseToFile(response, filePath) {
+  const arrayBuffer = await response.arrayBuffer();
+  const buffer = Buffer.from(arrayBuffer);
+  await writeFile(filePath, buffer);
+  return buffer.byteLength;
+}
+
+function findFirstFilePath(node, prefix = '') {
+  if (!node || typeof node !== 'object') return null;
+  const currentPath = prefix ? `${prefix}/${node.name}` : node.name;
+
+  if (!node.is_folder) {
+    return currentPath;
+  }
+
+  const children = node.children || {};
+  const childNames = Object.keys(children).sort();
+  for (const childName of childNames) {
+    const result = findFirstFilePath(children[childName], currentPath);
+    if (result) return result;
+  }
+
+  return null;
+}
+
+async function main() {
+  const runId = nowSlug();
+  const baseUrl = 'https://fc70-136-109-248-65.ngrok-free.app'; // Replace with ResilientDB ResShare backend URL
+  const password = 'Pass@123';
+  const owner = 'alice';
+  const receiver = 'bob';
+
+  await mkdir(OUTPUT_DIR, { recursive: true });
+
+  const ownerClient = new ResShareToolkitClient({ baseUrl });
+  const receiverClient = new ResShareToolkitClient({ baseUrl });
+
+  await safeSignup(ownerClient, { username: owner, password });
+  await safeSignup(receiverClient, { username: receiver, password });
+
+  const ownerLogin = await ownerClient.auth.login({ username: owner, password });
+  console.log(`login:${owner}`, ownerLogin.status);
+
+  const rootFolder = `meeting-demo-${runId}`;
+  const notesFolder = `${rootFolder}/notes`;
+  const contractsFolder = `${rootFolder}/contracts`;
+
+  await safeCreateFolder(ownerClient, rootFolder);
+  await safeCreateFolder(ownerClient, notesFolder);
+  await safeCreateFolder(ownerClient, contractsFolder);
+
+  if (typeof Blob === 'undefined') {
+    throw new Error('This example requires Blob support (Node.js >= 18).');
+  }
+
+  const meetingNotes = [
+    'ResShare Meeting Notes',
+    `Run ID: ${runId}`,
+    'Project: Phoenix',
+    'Launch date: 2026-03-15',
+    'Owner: Devang',
+    'Budget: $125k',
+    '',
+    'Action items:',
+    '- Upload demo artifacts',
+    '- Share folder with stakeholders',
+    '- Validate shared downloads'
+  ].join('\n');
+
+  const contractText = [
+    'Sample Contract Summary',
+    `Run ID: ${runId}`,
+    'Payment terms: Net 30',
+    'Termination: 30 days notice',
+    'Renewal: Annual'
+  ].join('\n');
+
+  const meetingNotesName = `meeting-notes-${runId}.txt`;
+  const contractName = `contract-summary-${runId}.txt`;
+
+  const uploadMeeting = await ownerClient.files.upload({
+    file: new Blob([meetingNotes], { type: 'text/plain' }),
+    filename: meetingNotesName,
+    path: notesFolder,
+    skipProcessing: false
+  });
+  console.log('upload', meetingNotesName, uploadMeeting.status, {
+    rag_processed: uploadMeeting.rag_processed,
+    rag_skipped: uploadMeeting.rag_skipped
+  });
+
+  const uploadContract = await ownerClient.files.upload({
+    file: new Blob([contractText], { type: 'text/plain' }),
+    filename: contractName,
+    path: contractsFolder,
+    skipProcessing: false
+  });
+  console.log('upload', contractName, uploadContract.status, {
+    rag_processed: uploadContract.rag_processed,
+    rag_skipped: uploadContract.rag_skipped
+  });
+
+  try {
+    const stats = await ownerClient._requestJson('/chat/stats', { method: 'GET' });
+    console.log('chat.stats', stats);
+    const chat = await ownerClient._requestJson('/chat', {
+      method: 'POST',
+      body: { query: `What is the Phoenix launch date for run ${runId}?` },
+      retries: 0
+    });
+    console.log('chat.answer', {
+      chunks_found: chat.chunks_found,
+      sources: chat.sources
+    });
+    console.log(chat.answer);
+  } catch (error) {
+    console.log('chat.error', error instanceof Error ? error.message : String(error));
+  }
+
+  const downloadPath = `${notesFolder}/${meetingNotesName}`;
+  const fileResponse = await ownerClient.files.download({ path: downloadPath });
+  const savedFile = path.join(OUTPUT_DIR, meetingNotesName);
+  const fileBytes = await responseToFile(fileResponse, savedFile);
+  console.log('download.saved', savedFile, `${fileBytes} bytes`);
+
+  const zipResponse = await ownerClient.files.downloadZip({ path: rootFolder });
+  const zipPath = path.join(OUTPUT_DIR, `${rootFolder}.zip`);
+  const zipBytes = await responseToFile(zipResponse, zipPath);
+  console.log('downloadZip.saved', zipPath, `${zipBytes} bytes`);
+
+  await safeShare(ownerClient, { target: receiver, path: rootFolder });
+
+  const receiverLogin = await receiverClient.auth.login({ username: receiver, password });
+  console.log(`login:${receiver}`, receiverLogin.status);
+
+  const shared = await receiverClient.shares.list();
+  console.log('shared.list.keys', Object.keys(shared.share_list || {}));
+
+  const fromUsers = Object.keys(shared.share_list || {}).sort();
+  const firstFromUser = fromUsers[0];
+  const firstNode = firstFromUser ? shared.share_list[firstFromUser]?.[0] : null;
+  const firstFilePath = firstNode ? findFirstFilePath(firstNode) : null;
+  if (!firstFromUser || !firstFilePath) {
+    throw new Error('No shared file discovered; share list was empty or only contained folders with no files.');
+  }
+
+  const combinedFilePath = `${firstFromUser}/${firstFilePath}`;
+  const sharedFileResponse = await receiverClient.files.download({ path: combinedFilePath, isShared: true });
+  const receiverSavedPath = path.join(OUTPUT_DIR, `shared-from-${firstFromUser}-${path.basename(firstFilePath)}`);
+  const sharedBytes = await responseToFile(sharedFileResponse, receiverSavedPath);
+  console.log('shared.download.saved', receiverSavedPath, `${sharedBytes} bytes`);
+
+  const combinedFolderPath = `${firstFromUser}/${firstNode.name}`;
+  const sharedZipResponse = await receiverClient.files.downloadZip({ path: combinedFolderPath, isShared: true });
+  const receiverZipPath = path.join(OUTPUT_DIR, `shared-from-${firstFromUser}-${firstNode.name}.zip`);
+  const sharedZipBytes = await responseToFile(sharedZipResponse, receiverZipPath);
+  console.log('shared.downloadZip.saved', receiverZipPath, `${sharedZipBytes} bytes`);
+
+  const removeResult = await receiverClient.shares.remove({ combinedPath: `${firstFromUser}/${firstNode.name}` });
+  console.log('shared.remove', removeResult.status);
+
+  await receiverClient.auth.deleteUser({ password });
+  console.log(`delete-user:${receiver}`, 'SUCCESS');
+  await ownerClient.auth.deleteUser({ password });
+  console.log(`delete-user:${owner}`, 'SUCCESS');
+}
+
+main().catch((error) => {
+  if (error instanceof ApiError) {
+    console.error('api.error', error.status, error.body);
+    process.exitCode = 1;
+    return;
+  }
+  console.error('example.failed', error instanceof Error ? error.stack || error.message : String(error));
+  process.exitCode = 1;
+});
diff --git a/ecosystem/tools/reshare-lib/examples/basic-usage.js b/ecosystem/tools/reshare-lib/examples/basic-usage.js
new file mode 100644
index 00000000..e61dd302
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/examples/basic-usage.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import ResShareToolkitClient from '../src/index.js';
+
+async function run() {
+  const client = new ResShareToolkitClient({
+    baseUrl: 'https://fc70-136-109-248-65.ngrok-free.app' // Replace with ResilientDB ResShare backend URL
+  });
+
+  await client.auth.signup({ username: 'alice', password: 'Pass@123' });
+  await client.auth.login({ username: 'alice', password: 'Pass@123' });
+
+  const folder = await client.files.createFolder('docs');
+  console.log('Create folder:', folder.status);
+
+  const shares = await client.shares.list();
+  console.log('Shared items:', shares.share_list || {});
+
+  await client.auth.logout();
+}
+
+run().catch(error => {
+  console.error('Example failed:', error);
+});
diff --git a/ecosystem/tools/reshare-lib/jest.config.cjs b/ecosystem/tools/reshare-lib/jest.config.cjs
new file mode 100644
index 00000000..53e56347
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/jest.config.cjs
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = {
+  testEnvironment: 'node',
+  roots: ['<rootDir>/tests'],
+  testMatch: ['**/*.test.js']
+};
diff --git a/ecosystem/tools/reshare-lib/package.json b/ecosystem/tools/reshare-lib/package.json
new file mode 100644
index 00000000..4c04e60c
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/package.json
@@ -0,0 +1,42 @@
+{
+  "name": "resshare-toolkit",
+  "version": "0.1.0",
+  "description": "Universal JavaScript toolkit for ResShare file sharing APIs",
+  "type": "module",
+  "main": "./src/index.js",
+  "exports": {
+    ".": "./src/index.js"
+  },
+  "files": [
+    "src",
+    "README.md",
+    "examples"
+  ],
+  "scripts": {
+    "test": "NODE_OPTIONS=--experimental-vm-modules jest"
+  },
+  "keywords": [
+    "resilientdb",
+    "reshare",
+    "file-sharing",
+    "toolkit",
+    "sdk",
+    "decentralized"
+  ],
+  "author": "ResilientDB Contributors",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/apache/incubator-resilientdb"
+  },
+  "bugs": {
+    "url": "https://github.com/apache/incubator-resilientdb/issues"
+  },
+  "homepage": "https://github.com/apache/incubator-resilientdb/tree/master/ecosystem/tools/reshare-lib",
+  "devDependencies": {
+    "jest": "^29.7.0"
+  },
+  "engines": {
+    "node": ">=18.0.0"
+  },
+  "license": "Apache-2.0"
+}
diff --git a/ecosystem/tools/reshare-lib/src/index.js b/ecosystem/tools/reshare-lib/src/index.js
new file mode 100644
index 00000000..3ce3a503
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/index.js
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import AuthClient from './modules/auth.js';
+import FilesClient from './modules/files.js';
+import SharesClient from './modules/shares.js';
+
+class ResShareToolkitClient {
+  constructor(config) {
+    if (!config || !config.baseUrl) {
+      throw new Error('ResShareToolkitClient requires a baseUrl in config');
+    }
+
+    const baseUrl = String(config.baseUrl).replace(/\/+$/, '');
+    const apiPrefix = this._normalizePrefix(config.apiPrefix);
+
+    this.config = {
+      baseUrl,
+      apiPrefix,
+      timeout: Number.isFinite(config.timeout) ? config.timeout : 30000,
+      retries: Number.isFinite(config.retries) ? config.retries : 2,
+      onAuthExpired: config.onAuthExpired || null,
+      credentials: config.credentials || 'include',
+      cookieJarEnabled: config.cookieJarEnabled
+    };
+
+    this.apiBase = `${this.config.baseUrl}${this.config.apiPrefix}`;
+
+    this._fetch = config.fetch || globalThis.fetch;
+    this._FormData = config.FormData || globalThis.FormData;
+
+    if (!this._fetch) {
+      throw new Error('ResShareToolkitClient requires fetch (provide config.fetch in Node)');
+    }
+
+    this._token = null;
+    this._cookieJarEnabled = this.config.cookieJarEnabled;
+    if (this._cookieJarEnabled === undefined) {
+      this._cookieJarEnabled = typeof window === 'undefined';
+    }
+    this._cookieStore = new Map();
+
+    this.auth = new AuthClient(this);
+    this.files = new FilesClient(this);
+    this.shares = new SharesClient(this);
+  }
+
+  setToken(token) {
+    this._token = token || null;
+  }
+
+  clearToken() {
+    this._token = null;
+  }
+
+  getToken() {
+    return this._token;
+  }
+
+  async _requestJson(path, options = {}) {
+    return this._request(path, { ...options, responseType: 'json' });
+  }
+
+  async _requestRaw(path, options = {}) {
+    return this._request(path, { ...options, responseType: 'raw' });
+  }
+
+  _createFormData() {
+    if (!this._FormData) {
+      throw new Error('FormData is not available (provide config.FormData in Node)');
+    }
+    return new this._FormData();
+  }
+
+  _buildUrl(path) {
+    if (!path) {
+      return this.apiBase;
+    }
+    const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+    return `${this.apiBase}${normalizedPath}`;
+  }
+
+  _buildCookieHeader() {
+    if (!this._cookieJarEnabled || !this._cookieStore.size) {
+      return '';
+    }
+    return Array.from(this._cookieStore.entries())
+      .map(([name, value]) => `${name}=${value}`)
+      .join('; ');
+  }
+
+  _storeCookies(response) {
+    if (!this._cookieJarEnabled || !response?.headers) {
+      return;
+    }
+
+    let cookies = [];
+    if (typeof response.headers.getSetCookie === 'function') {
+      cookies = response.headers.getSetCookie();
+    } else {
+      const header = response.headers.get('set-cookie');
+      if (header) {
+        cookies = [header];
+      }
+    }
+
+    cookies.forEach((cookie) => {
+      const pair = cookie.split(';')[0];
+      const separatorIndex = pair.indexOf('=');
+      if (separatorIndex <= 0) {
+        return;
+      }
+      const name = pair.slice(0, separatorIndex).trim();
+      const value = pair.slice(separatorIndex + 1).trim();
+      if (name) {
+        this._cookieStore.set(name, value);
+      }
+    });
+  }
+
+  _normalizePrefix(prefix) {
+    if (!prefix) {
+      return '';
+    }
+    const trimmed = String(prefix).trim();
+    if (!trimmed) {
+      return '';
+    }
+    const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
+    return withSlash.replace(/\/+$/, '');
+  }
+
+  _normalizeResponse(data) {
+    if (!data || typeof data !== 'object' || Array.isArray(data)) {
+      return data;
+    }
+
+    if (data.status == null) {
+      const status = data.result || data.message;
+      if (status) {
+        return { ...data, status };
+      }
+    }
+
+    return data;
+  }
+
+  async _request(path, options = {}) {
+    const url = this._buildUrl(path);
+    const method = options.method || 'GET';
+    const headers = { ...(options.headers || {}) };
+    const maxRetries = Number.isFinite(options.retries) ? options.retries : this.config.retries;
+    const responseType = options.responseType || 'json';
+
+    let body = options.body;
+    const isFormData = this._FormData && body instanceof this._FormData;
+    const isString = typeof body === 'string';
+    const isBlob = typeof Blob !== 'undefined' && body instanceof Blob;
+    const isBinary = isBlob || body instanceof ArrayBuffer || body instanceof Uint8Array;
+
+    if (body != null && !isFormData && !isString && !isBinary) {
+      if (!headers['Content-Type']) {
+        headers['Content-Type'] = 'application/json';
+      }
+      body = JSON.stringify(body);
+    }
+
+    if (this._token && !headers.Authorization) {
+      headers.Authorization = `Bearer ${this._token}`;
+    }
+
+    const cookieHeader = this._buildCookieHeader();
+    if (cookieHeader) {
+      if (headers.Cookie) {
+        headers.Cookie = `${headers.Cookie}; ${cookieHeader}`;
+      } else {
+        headers.Cookie = cookieHeader;
+      }
+    }
+
+    const fetchOptions = {
+      method,
+      headers,
+      body,
+      credentials: options.credentials || this.config.credentials
+    };
+
+    let lastError = null;
+    let refreshed = false;
+
+    for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
+      try {
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
+
+        const response = await this._fetch(url, {
+          ...fetchOptions,
+          signal: controller.signal
+        });
+
+        clearTimeout(timeoutId);
+        this._storeCookies(response);
+
+        if (response.ok) {
+          if (responseType === 'raw') {
+            return response;
+          }
+
+          const contentType = response.headers.get('content-type') || '';
+          if (contentType.includes('application/json')) {
+            const data = await response.json();
+            return this._normalizeResponse(data);
+          }
+
+          return null;
+        }
+
+        let errorBody = null;
+        try {
+          errorBody = await response.json();
+        } catch (error) {
+          errorBody = { message: response.statusText };
+        }
+
+        if (response.status === 401 && this.config.onAuthExpired && !refreshed) {
+          refreshed = true;
+          const newToken = await this.config.onAuthExpired();
+          if (newToken) {
+            this.setToken(newToken);
+            attempt -= 1;
+            continue;
+          }
+        }
+
+        throw new ApiError(response.status, this._normalizeResponse(errorBody));
+      } catch (error) {
+        lastError = error;
+
+        if (error instanceof ApiError) {
+          if ([400, 401, 403, 404].includes(error.status)) {
+            throw error;
+          }
+        }
+
+        if (attempt === maxRetries) {
+          break;
+        }
+
+        await this._sleep(Math.pow(2, attempt) * 250);
+      }
+    }
+
+    throw lastError || new Error('Request failed after retries');
+  }
+
+  _sleep(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+  }
+}
+
+class ApiError extends Error {
+  constructor(status, body, message = null) {
+    super(message || body?.message || body?.status || `API error: ${status}`);
+    this.name = 'ApiError';
+    this.status = status;
+    this.body = body;
+  }
+
+  isAuthError() {
+    return this.status === 401;
+  }
+
+  isValidationError() {
+    return this.status === 400;
+  }
+
+  getUserMessage() {
+    if (this.body?.errors && typeof this.body.errors === 'object') {
+      return Object.entries(this.body.errors)
+        .map(([field, msg]) => `${field}: ${msg}`)
+        .join(', ');
+    }
+    return this.body?.message || this.message;
+  }
+}
+
+export { ResShareToolkitClient, ApiError };
+export default ResShareToolkitClient;
diff --git a/ecosystem/tools/reshare-lib/src/modules/auth.js b/ecosystem/tools/reshare-lib/src/modules/auth.js
new file mode 100644
index 00000000..5fc74258
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/modules/auth.js
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class AuthClient {
+  constructor(client) {
+    this.client = client;
+  }
+
+  async login({ username, password }) {
+    return this.client._requestJson('/login', {
+      method: 'POST',
+      body: { username, password },
+      retries: 0
+    });
+  }
+
+  async signup({ username, password }) {
+    return this.client._requestJson('/signup', {
+      method: 'POST',
+      body: { username, password },
+      retries: 0
+    });
+  }
+
+  async logout() {
+    return this.client._requestJson('/logout', {
+      method: 'POST'
+    });
+  }
+
+  async deleteUser({ password }) {
+    return this.client._requestJson('/delete-user', {
+      method: 'DELETE',
+      body: { password }
+    });
+  }
+
+  async status() {
+    return this.client._requestJson('/auth-status', {
+      method: 'GET'
+    });
+  }
+}
+
+export default AuthClient;
diff --git a/ecosystem/tools/reshare-lib/src/modules/files.js b/ecosystem/tools/reshare-lib/src/modules/files.js
new file mode 100644
index 00000000..d85eb0e4
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/modules/files.js
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class FilesClient {
+  constructor(client) {
+    this.client = client;
+  }
+
+  async createFolder(path) {
+    return this.client._requestJson('/create-folder', {
+      method: 'POST',
+      body: { folder_path: path }
+    });
+  }
+
+  async upload({ file, path = '', skipProcessing = true, filename }) {
+    if (!file) {
+      throw new Error('upload requires a file');
+    }
+
+    const formData = this.client._createFormData();
+    const resolvedName = filename || file.name;
+
+    if (resolvedName) {
+      formData.append('file', file, resolvedName);
+    } else {
+      formData.append('file', file);
+    }
+
+    formData.append('path', path || '');
+    formData.append('skip_ai_processing', String(Boolean(skipProcessing)));
+
+    return this.client._requestJson('/upload', {
+      method: 'POST',
+      body: formData,
+      retries: 0
+    });
+  }
+
+  async deleteItem({ path, deleteInRoot = true }) {
+    return this.client._requestJson('/delete', {
+      method: 'DELETE',
+      body: {
+        node_path: path,
+        delete_in_root: deleteInRoot
+      }
+    });
+  }
+
+  async download({ path, isShared = false }) {
+    return this.client._requestRaw('/download', {
+      method: 'POST',
+      body: {
+        path,
+        is_shared: isShared
+      }
+    });
+  }
+
+  async downloadZip({ path, isShared = false }) {
+    return this.client._requestRaw('/download-zip', {
+      method: 'POST',
+      body: {
+        path,
+        is_shared: isShared
+      }
+    });
+  }
+}
+
+export default FilesClient;
diff --git a/ecosystem/tools/reshare-lib/src/modules/shares.js b/ecosystem/tools/reshare-lib/src/modules/shares.js
new file mode 100644
index 00000000..17ee9b0b
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/src/modules/shares.js
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class SharesClient {
+  constructor(client) {
+    this.client = client;
+  }
+
+  async share({ target, path, node }) {
+    if (!target) {
+      throw new Error('share requires a target username');
+    }
+
+    const payload = { target };
+
+    if (path) {
+      payload.path = path;
+    }
+
+    if (node) {
+      payload.node = typeof node === 'string' ? node : JSON.stringify(node);
+    }
+
+    return this.client._requestJson('/share', {
+      method: 'POST',
+      body: payload
+    });
+  }
+
+  async list() {
+    return this.client._requestJson('/shared', {
+      method: 'GET'
+    });
+  }
+
+  async remove({ combinedPath, fromUser, path }) {
+    const nodePath = combinedPath || (fromUser && path ? `${fromUser}/${path}` : null);
+    if (!nodePath) {
+      throw new Error('remove requires combinedPath or fromUser and path');
+    }
+
+    return this.client._requestJson('/delete', {
+      method: 'DELETE',
+      body: {
+        node_path: nodePath,
+        delete_in_root: false
+      }
+    });
+  }
+}
+
+export default SharesClient;
diff --git a/ecosystem/tools/reshare-lib/tests/unit/client.test.js b/ecosystem/tools/reshare-lib/tests/unit/client.test.js
new file mode 100644
index 00000000..21dc51e0
--- /dev/null
+++ b/ecosystem/tools/reshare-lib/tests/unit/client.test.js
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import ResShareToolkitClient from '../../src/index.js';
+
+class MockFormData {
+  constructor() {
+    this.fields = [];
+  }
+
+  append(name, value, filename) {
+    this.fields.push({ name, value, filename });
+  }
+}
+
+const jsonResponse = (data, status = 200) => ({
+  ok: status >= 200 && status < 300,
+  status,
+  headers: {
+    get: (name) => (name.toLowerCase() === 'content-type' ? 'application/json' : null)
+  },
+  json: async () => data
+});
+
+const jsonResponseWithCookies = (data, cookies, status = 200) => ({
+  ok: status >= 200 && status < 300,
+  status,
+  headers: {
+    get: (name) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
+    getSetCookie: () => cookies
+  },
+  json: async () => data
+});
+
+describe('ResShareToolkitClient', () => {
+  test('requires baseUrl', () => {
+    expect(() => new ResShareToolkitClient({})).toThrow(/baseUrl/);
+  });
+
+  test('builds apiBase with prefix', () => {
+    const client = new ResShareToolkitClient({
+      baseUrl: 'http://localhost:5000/',
+      apiPrefix: '/api/v1',
+      fetch: async () => jsonResponse({}),
+      FormData: MockFormData
+    });
+
+    expect(client.apiBase).toBe('http://localhost:5000/api/v1');
+  });
+
+  test('login sends JSON body and credentials', async () => {
+    let captured = null;
+    const fetch = async (url, options) => {
+      captured = { url, options };
+      return jsonResponse({ message: 'SUCCESS' });
+    };
+
+    const client = new ResShareToolkitClient({
+      baseUrl: 'http://localhost:5000',
+      fetch,
+      FormData: MockFormData
+    });
+
+    await client.auth.login({ username: 'alice', password: 'secret' });
+
+    expect(captured.url).toBe('http://localhost:5000/login');
+    expect(captured.options.method).toBe('POST');
+    expect(captured.options.credentials).toBe('include');
+    expect(captured.options.headers['Content-Type']).toBe('application/json');
+    expect(JSON.parse(captured.options.body)).toEqual({
+      username: 'alice',
+      password: 'secret'
+    });
+  });
+
+  test('upload builds form data payload', async () => {
+    let captured = null;
+    const fetch = async (url, options) => {
+      captured = { url, options };
+      return jsonResponse({ message: 'SUCCESS' });
+    };
+
+    const client = new ResShareToolkitClient({
+      baseUrl: 'http://localhost:5000',
+      fetch,
+      FormData: MockFormData
+    });
+
+    const file = { name: 'doc.txt' };
+    await client.files.upload({ file, path: 'docs', skipProcessing: true });
+
+    expect(captured.url).toBe('http://localhost:5000/upload');
+    expect(captured.options.method).toBe('POST');
+    expect(captured.options.body instanceof MockFormData).toBe(true);
+
+    const fields = captured.options.body.fields;
+    const fieldMap = Object.fromEntries(fields.map((field) => [field.name, field]));
+
+    expect(fieldMap.path.value).toBe('docs');
+    expect(fieldMap.skip_ai_processing.value).toBe('true');
+    expect(fieldMap.file.filename).toBe('doc.txt');
+  });
+
+  test('remove share builds combined path', async () => {
+    let captured = null;
+    const fetch = async (url, options) => {
+      captured = { url, options };
+      return jsonResponse({ message: 'SUCCESS' });
+    };
+
+    const client = new ResShareToolkitClient({
+      baseUrl: 'http://localhost:5000',
+      fetch,
+      FormData: MockFormData
+    });
+
+    await client.shares.remove({ fromUser: 'alice', path: 'docs' });
+
+    expect(captured.url).toBe('http://localhost:5000/delete');
+    expect(captured.options.method).toBe('DELETE');
+    expect(JSON.parse(captured.options.body)).toEqual({
+      node_path: 'alice/docs',
+      delete_in_root: false
+    });
+  });
+
+  test('persists session cookie between requests in Node', async () => {
+    const calls = [];
+    const fetch = async (url, options) => {
+      calls.push({ url, options });
+      if (calls.length === 1) {
+        return jsonResponseWithCookies({ message: 'SUCCESS' }, ['session=abc123; Path=/; HttpOnly']);
+      }
+      return jsonResponse({ message: 'SUCCESS' });
+    };
+
+    const client = new ResShareToolkitClient({
+      baseUrl: 'http://localhost:5000',
+      fetch,
+      FormData: MockFormData
+    });
+
+    await client.auth.login({ username: 'alice', password: 'secret' });
+    await client.files.createFolder('docs');
+
+    expect(calls[1].options.headers.Cookie).toBe('session=abc123');
+  });
+});
diff --git a/platform/consensus/recovery/recovery.cpp b/platform/consensus/recovery/recovery.cpp
index bf24ebc7..9e37f6eb 100644
--- a/platform/consensus/recovery/recovery.cpp
+++ b/platform/consensus/recovery/recovery.cpp
@@ -452,9 +452,11 @@ void Recovery::ReadLogs(
   if (recovery_enabled_ == false) {
     return;
   }
-  assert(storage_);
-  int64_t storage_ckpt = storage_->GetLastCheckpoint();
-  LOG(ERROR) << " storage ckpt:" << storage_ckpt;
+
+  int64_t storage_ckpt = 0;
+  if(storage_) {
+    storage_ckpt = storage_->GetLastCheckpoint();
+  }
   std::unique_lock<std::mutex> lk(mutex_);
 
   auto recovery_files_pair = GetRecoveryFiles(storage_ckpt);
diff --git a/platform/consensus/recovery/recovery_test.cpp b/platform/consensus/recovery/recovery_test.cpp
index 6c0dd25b..f6aeba47 100644
--- a/platform/consensus/recovery/recovery_test.cpp
+++ b/platform/consensus/recovery/recovery_test.cpp
@@ -83,7 +83,7 @@ TEST_F(RecoveryTest, ReadLog) {
 
   std::vector<int> expected_types = {
       Request::TYPE_PRE_PREPARE, Request::TYPE_PREPARE, Request::TYPE_COMMIT,
-      Request::TYPE_CHECKPOINT,  Request::TYPE_NEWVIEW,
+      Request::TYPE_NEWVIEW,
   };
 
   {
@@ -123,7 +123,7 @@ TEST_F(RecoveryTest, ReadLog_FlushOnce) {
 
   std::vector<int> expected_types = {
       Request::TYPE_PRE_PREPARE, Request::TYPE_PREPARE, Request::TYPE_COMMIT,
-      Request::TYPE_CHECKPOINT,  Request::TYPE_NEWVIEW,
+      Request::TYPE_NEWVIEW,
   };
 
   {
@@ -142,7 +142,6 @@ TEST_F(RecoveryTest, ReadLog_FlushOnce) {
     recovery.ReadLogs([&](const SystemInfoData &data) {},
                       [&](std::unique_ptr<Context> context,
                           std::unique_ptr<Request> request) {
-                        LOG(ERROR) << "call back:" << request->seq();
                         list.push_back(*request);
                       },
                       nullptr);