UNPKG

@inkwell.ar/sdk

Version:

SDK for interacting with the Inkwell Blog CRUD AO process using aoconnect

659 lines (524 loc) 16.4 kB
# @inkwell.ar/sdk A TypeScript SDK for interacting with the Inkwell Blog CRUD AO process using aoconnect. Published as `@inkwell.ar/sdk`. ## Features - 🔐 **Role-based Access Control**: Support for Editor and Admin roles - 📝 **Full CRUD Operations**: Create, read, update, and delete blog posts - 👥 **User Management**: Add/remove editors and admins - 🎨 **Blog Customization**: Set blog title, description, and logo - 🚀 **Easy Deployment**: Deploy your own blog process with one command - ✅ **Type Safety**: Full TypeScript support with comprehensive type definitions - 🛡️ **Input Validation**: Built-in validation for all inputs - 🔄 **Error Handling**: Comprehensive error handling and response parsing ## Installation ```bash npm install @inkwell.ar/sdk ``` Or using yarn: ```bash yarn add @inkwell.ar/sdk ``` ## Quick Start ```typescript import { InkwellBlogSDK } from '@inkwell.ar/sdk'; // Initialize the SDK const blogSDK = new InkwellBlogSDK({ processId: 'your-ao-process-id-here' }); // Get all posts const response = await blogSDK.getAllPosts({ ordered: true }); if (response.success) { console.log('Posts:', response.data); } ``` ## API Reference ### Initialization ```typescript import { InkwellBlogSDK } from '@inkwell.ar/sdk'; const blogSDK = new InkwellBlogSDK({ processId: string, // Required: Your AO process ID wallet?: any, // Optional: Arweave wallet for authenticated operations aoconnect?: any // Optional: Custom aoconnect instance }); ``` **Browser Wallet Support**: In browser environments, if no wallet is provided, the SDK will automatically use `globalThis.arweaveWallet` if available. ### Public Methods (No Authentication Required) #### `getInfo()` Get blog information including name, author, and details. ```typescript const response = await blogSDK.getInfo(); // Returns: { name, author, blogTitle, blogDescription, blogLogo, details } ``` #### `getAllPosts(options?)` Get all blog posts. ```typescript const response = await blogSDK.getAllPosts({ ordered: true // Optional: Sort by published_at (newest first) }); ``` #### `getPost(options)` Get a specific post by ID. ```typescript const response = await blogSDK.getPost({ id: 1 }); ``` #### `getUserRoles()` Get roles for the current wallet. ```typescript const response = await blogSDK.getUserRoles(); ``` ### Editor Methods (Requires Editor Role) #### `createPost(options)` Create a new blog post. ```typescript const response = await blogSDK.createPost({ data: { title: 'My Blog Post', description: 'A brief description', body: 'Full post content...', published_at: Date.now(), last_update: Date.now(), labels: ['tag1', 'tag2'], authors: ['@author1', '@author2'] }, wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` #### `updatePost(options)` Update an existing blog post. ```typescript const response = await blogSDK.updatePost({ id: 1, data: { title: 'Updated Title', description: 'Updated description', // ... other fields }, wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` #### `deletePost(options)` Delete a blog post. ```typescript const response = await blogSDK.deletePost({ id: 1, wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` ### Admin Methods (Requires Admin Role) #### `addEditors(options)` Add new editors to the blog. ```typescript const response = await blogSDK.addEditors({ accounts: ['editor-address-1', 'editor-address-2'], wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` #### `removeEditors(options)` Remove editors from the blog. ```typescript const response = await blogSDK.removeEditors({ accounts: ['editor-address-to-remove'], wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` #### `addAdmins(options)` Add new admins to the blog. ```typescript const response = await blogSDK.addAdmins({ accounts: ['admin-address-1', 'admin-address-2'], wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` #### `removeAdmins(options)` Remove admins from the blog. ```typescript const response = await blogSDK.removeAdmins({ accounts: ['admin-address-to-remove'], wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` #### `getEditors()` Get all current editors. ```typescript const response = await blogSDK.getEditors(); ``` #### `getAdmins()` Get all current admins. ```typescript const response = await blogSDK.getAdmins(); ``` #### `setBlogDetails(options)` Set blog details (title, description, logo). Admin role required. ```typescript const response = await blogSDK.setBlogDetails({ data: { title: 'My Blog Title', description: 'My blog description', logo: 'https://example.com/logo.png' }, wallet: yourWallet // Optional: will use browser wallet if not provided }); ``` ## Data Types ### BlogPost ```typescript interface BlogPost { id: number; title: string; description: string; body?: string; published_at: number; last_update: number; labels?: string[]; authors: string[]; } ``` ### ApiResponse ```typescript interface ApiResponse<T = any> { success: boolean; data: T | string; // T for successful operations, string for error messages } ``` ### CreatePostData ```typescript interface CreatePostData { title: string; description: string; body?: string; published_at: number; last_update: number; labels?: string[]; authors: string[]; } ``` ### BlogDetails ```typescript interface BlogDetails { title: string; description: string; logo: string; } ``` ### BlogInfo ```typescript interface BlogInfo { name: string; author: string; blogTitle: string; blogDescription: string; blogLogo: string; details: BlogDetails; } ``` ### UpdateBlogDetailsData ```typescript interface UpdateBlogDetailsData { title?: string; description?: string; logo?: string; } ``` ## Deployment The SDK includes built-in deployment functionality using [ao-deploy](https://github.com/pawanpaudel93/ao-deploy). ### Deploy a New Blog Process ```typescript import { InkwellBlogSDK } from '@inkwell.ar/sdk'; // Deploy a new blog process const deployResult = await InkwellBlogSDK.deploy({ name: 'my-blog', wallet: yourWallet, contractPath: './lua-process/inkwell_blog.lua', luaPath: './lua-process/?.lua', tags: [ { name: 'Blog-Name', value: 'My Personal Blog' }, { name: 'Author', value: '@myhandle' } ], minify: true }); console.log('Process ID:', deployResult.processId); console.log('View at:', `https://www.ao.link/#/entity/${deployResult.processId}`); // Initialize SDK with the new process const blogSDK = new InkwellBlogSDK({ processId: deployResult.processId, wallet: yourWallet }); ``` ### Deployment Options ```typescript interface DeployOptions { name?: string; // Process name (default: 'inkwell-blog') wallet?: string | any; // Arweave wallet contractPath?: string; // Path to process file luaPath?: string; // Path to Lua modules tags?: Array<{ name: string; value: string }>; // Custom tags retry?: { count: number; delay: number }; // Retry configuration minify?: boolean; // Minify contract (default: true) contractTransformer?: (source: string) => string; // Custom source transformer onBoot?: boolean; // Load on boot (default: false) silent?: boolean; // Disable logging (default: false) blueprints?: string[]; // Blueprints to use forceSpawn?: boolean; // Force new process (default: false) } ``` ## Examples ### Basic Usage ```typescript import { InkwellBlogSDK } from '@inkwell.ar/sdk'; const blogSDK = new InkwellBlogSDK({ processId: 'your-process-id' }); // Get blog information const infoResponse = await blogSDK.getInfo(); if (infoResponse.success) { const info = infoResponse.data; console.log(`Blog: ${info.name} by ${info.author}`); console.log(`Title: ${info.blogTitle}`); console.log(`Description: ${info.blogDescription}`); } // Get all posts const postsResponse = await blogSDK.getAllPosts({ ordered: true }); if (postsResponse.success) { postsResponse.data.forEach(post => { console.log(`${post.title} by ${post.authors.join(', ')}`); }); } ``` ### With Wallet Authentication ```typescript import { InkwellBlogSDK } from '@inkwell.ar/sdk'; import Arweave from 'arweave'; const arweave = Arweave.init(); // Load or generate wallet (automatically saves to wallet.json) let wallet: any; const walletPath = 'wallet.json'; try { const fs = require('fs'); if (fs.existsSync(walletPath)) { console.log('Loading existing wallet...'); const walletData = fs.readFileSync(walletPath, 'utf8'); wallet = JSON.parse(walletData); } else { console.log('Generating new wallet...'); wallet = await arweave.wallets.generate(); fs.writeFileSync(walletPath, JSON.stringify(wallet, null, 2)); console.log('Wallet saved to wallet.json'); } } catch (error) { console.log('Generating new wallet due to error...'); wallet = await arweave.wallets.generate(); } const blogSDK = new InkwellBlogSDK({ processId: 'your-process-id', wallet: wallet }); ``` // Set blog details (requires Admin role) const blogDetailsResponse = await blogSDK.setBlogDetails({ data: { title: 'My Personal Blog', description: 'A blog about technology and life', logo: 'https://example.com/logo.png' }, wallet: wallet }); // Create a new post (requires Editor role) const createResponse = await blogSDK.createPost({ data: { title: 'My First Post', description: 'Hello World!', body: 'This is my first blog post.', published_at: Date.now(), last_update: Date.now(), authors: ['@myhandle'] }, wallet: wallet }); ``` ### Error Handling ```typescript try { const response = await blogSDK.createPost({ data: invalidData, wallet: wallet }); if (!response.success) { console.error('Operation failed:', response.data); } } catch (error) { console.error('Unexpected error:', error); } ``` ## Wallet Management ### Node.js Environment The SDK examples automatically handle wallet management: - **Default wallet file**: `wallet.json` in the project root - **Auto-generation**: If no wallet exists, a new one is generated and saved - **Persistence**: Wallet is saved to file for future use - **Error handling**: Falls back to generating a new wallet if file operations fail **⚠️ Security Note**: The `wallet.json` file contains your private keys. Keep it secure and never commit it to version control. ### Browser Environment In browser environments, the SDK supports automatic browser wallet detection: ```typescript // No wallet needed - SDK will use browser wallet automatically const response = await blogSDK.createPost({ data: { title: 'My Post', description: 'Post description', body: 'Post content...', published_at: Date.now(), last_update: Date.now(), authors: ['@myhandle'] } // wallet parameter is optional in browser }); ``` **Browser Wallet Support**: The SDK automatically detects and uses `globalThis.arweaveWallet` if available, making wallet management seamless in browser applications. ## Role System The blog uses a role-based access control system: - **Public**: Anyone can read posts and check their own roles - **Editor**: Can create, update, and delete posts - **Admin**: Can manage roles (add/remove editors and admins) ### Checking Roles ```typescript const rolesResponse = await blogSDK.getUserRoles(); if (rolesResponse.success) { const roles = rolesResponse.data; if (roles.includes('EDITOR_ROLE')) { // User can create/edit posts } if (roles.includes('DEFAULT_ADMIN_ROLE')) { // User can manage roles } } ``` ## Message Result Retrieval The SDK automatically attempts to retrieve the actual result of message operations: 1. **Message Sent**: First, the message is sent to the AO process 2. **Result Retrieval**: The SDK then attempts to get the result using the message ID 3. **Fallback**: If result retrieval fails, a success message with the message ID is returned This provides the best of both worlds: - **Immediate feedback**: You know the message was sent successfully - **Actual data**: When possible, you get the parsed result from the process - **Graceful degradation**: If result retrieval fails, you still get confirmation ### Parsing Logic The SDK uses a unified parsing approach that handles both dryrun and message responses: - **Dryrun responses**: Parsed directly from the AO process response - **Message responses**: Parsed with recursive support for nested JSON structures - **Smart fallback**: Returns the raw response if parsing fails ### Return Types Write operations can return either the actual data or a success message: ```typescript // createPost and updatePost can return: type CreatePostResponse = ApiResponse<BlogPost | string>; // addEditors, removeEditors, addAdmins, removeAdmins can return: type RoleResponse = ApiResponse<RoleUpdateResult[] | string>; // setBlogDetails can return: type BlogDetailsResponse = ApiResponse<BlogDetails | string>; // deletePost always returns: type DeleteResponse = ApiResponse<string>; ``` ### RoleUpdateResult The `RoleUpdateResult` type represents the result of role management operations: ```typescript type RoleUpdateResult = [string, boolean, string?]; // [account, success, error?] ``` - `account`: The wallet address that was processed - `success`: Whether the operation succeeded - `error`: Optional error message if the operation failed ### Handling Dual Return Types ```typescript const response = await blogSDK.createPost({ data: postData }); if (response.success) { if (typeof response.data === 'object' && response.data !== null) { // Got the actual post data const post = response.data as BlogPost; console.log(`Post ID: ${post.id}`); console.log(`Title: ${post.title}`); } else { // Got a success message console.log(`Message: ${response.data}`); } } // For role management operations: const roleResponse = await blogSDK.addEditors({ accounts: ['address1'], wallet }); if (roleResponse.success) { if (Array.isArray(roleResponse.data)) { // Got the actual role update results const results = roleResponse.data as RoleUpdateResult[]; results.forEach(([account, success, error]) => { if (success) { console.log(`✅ Successfully added editor: ${account}`); } else { console.log(`❌ Failed to add editor ${account}: ${error}`); } }); } else { // Got a success message console.log(`Message: ${roleResponse.data}`); } } ``` ## Error Handling The SDK provides comprehensive error handling: 1. **Validation Errors**: Invalid input data 2. **Permission Errors**: Insufficient role permissions 3. **Network Errors**: Connection issues with AO 4. **Process Errors**: Errors from the AO process itself 5. **Wallet Errors**: Missing or invalid wallet configuration All methods return an `ApiResponse` object with: - `success`: Boolean indicating if the operation succeeded - `data`: The result data or error message ## Development ### Building ```bash npm run build ``` ### Development Mode ```bash npm run dev ``` ### Testing ```bash npm test ``` ### Linting ```bash npm run lint ``` ### Publishing to npm Before publishing, make sure to: 1. **Build the package:** ```bash npm run build ``` 2. **Test the build:** ```bash npm test ``` 3. **Check package contents:** ```bash npm pack --dry-run ``` 4. **Publish to npm:** ```bash npm publish ``` The `prepublishOnly` script will automatically run the build before publishing. ## Package Contents The npm package includes: - **Compiled TypeScript** - Ready-to-use JavaScript files in `dist/` - **Type Definitions** - Full TypeScript support with `.d.ts` files - **Lua Process Files** - The `lua-process/` directory with your AO process files - **Documentation** - This README file The package excludes: - Source TypeScript files (`src/`) - Examples and tests - Development configuration files - Build artifacts ## License MIT ## Author [@7i7o](https://github.com/7i7o)