@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
- 🔗 Blog Registry: Centralized permission tracking across multiple blogs
- 🔒 Security-First: Blog-only write access to registry, read-only SDK
- ✅ 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
npm install @inkwell.ar/sdk
Or using yarn:
yarn add @inkwell.ar/sdk
Browser Compatibility
The SDK is designed to work in both browser and Node.js environments:
- ✅ Browser: Full functionality including deployment
- ✅ Node.js: Full functionality including deployment
- ✅ Both: Read operations, blog interactions, registry queries
The SDK uses aoconnect for deployment in all environments, ensuring consistent behavior.
Quick Start
Browser Environment
The SDK works in browsers for all operations including deployment:
import { InkwellBlogSDK, BlogRegistrySDK } from '@inkwell.ar/sdk';
const result = await InkwellBlogSDK.deploy({
name: 'my-blog'
});
const blogSDK = new InkwellBlogSDK({
processId: result.processId
});
const registry = new BlogRegistrySDK();
const response = await blogSDK.getAllPosts({ ordered: true });
if (response.success) {
console.log('Posts:', response.data);
}
const canEdit = await registry.canEditBlog('wallet-address', result.processId);
const canAdmin = await registry.canAdminBlog('wallet-address', result.processId);
const userBlogs = await registry.getWalletBlogs('wallet-address');
Node.js Environment
For Node.js environments (same functionality as browser, uses aoconnect):
1. Deploy the Blog Registry (One-time setup)
npm run deploy:registry [wallet-path]
This will:
- Deploy the registry process to AO
- Test the deployment
- Save the process ID to
src/config/registry.ts
- Output the process ID for verification
2. Deploy a Blog
import { InkwellBlogSDK } from '@inkwell.ar/sdk';
const result = await InkwellBlogSDK.deploy({
name: 'my-blog'
});
console.log('Blog deployed:', result.processId);
3. Use the Blog and Registry
import { InkwellBlogSDK, BlogRegistrySDK } from '@inkwell.ar/sdk';
const blogSDK = new InkwellBlogSDK({
processId: result.processId
});
const registry = new BlogRegistrySDK();
const response = await blogSDK.getAllPosts({ ordered: true });
if (response.success) {
console.log('Posts:', response.data);
}
const canEdit = await registry.canEditBlog('wallet-address', result.processId);
const canAdmin = await registry.canAdminBlog('wallet-address', result.processId);
const userBlogs = await registry.getWalletBlogs('wallet-address');
API Reference
Initialization
import { InkwellBlogSDK } from '@inkwell.ar/sdk';
const blogSDK = new InkwellBlogSDK({
processId: string,
wallet?: any,
aoconnect?: any
});
Browser Wallet Support: In browser environments, if no wallet is provided, the SDK will automatically use globalThis.arweaveWallet if available.
Blog Registry SDK
The registry SDK provides read-only access to the centralized permission system:
import { BlogRegistrySDK } from '@inkwell.ar/sdk';
const registry = new BlogRegistrySDK();
const canEdit = await registry.canEditBlog('wallet-address', 'blog-process-id');
const canAdmin = await registry.canAdminBlog('wallet-address', 'blog-process-id');
const userBlogs = await registry.getWalletBlogs('wallet-address');
const adminBlogs = await registry.getAdminBlogs('wallet-address');
const editableBlogs = await registry.getEditableBlogs('wallet-address');
const stats = await registry.getRegistryStats();
Security Note: The registry SDK is read-only. Only blog processes can modify permissions in the registry.
Public Methods (No Authentication Required)
getInfo()
Get blog information including name, author, and details.
const response = await blogSDK.getInfo();
getAllPosts(options?)
Get all blog posts.
const response = await blogSDK.getAllPosts({
ordered: true
});
getPost(options)
Get a specific post by ID.
const response = await blogSDK.getPost({
id: 1
});
getUserRoles()
Get roles for the current wallet.
const response = await blogSDK.getUserRoles();
Editor Methods (Requires Editor Role)
createPost(options)
Create a new blog post.
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
});
updatePost(options)
Update an existing blog post.
const response = await blogSDK.updatePost({
id: 1,
data: {
title: 'Updated Title',
description: 'Updated description',
},
wallet: yourWallet
});
deletePost(options)
Delete a blog post.
const response = await blogSDK.deletePost({
id: 1,
wallet: yourWallet
});
Admin Methods (Requires Admin Role)
addEditors(options)
Add new editors to the blog.
const response = await blogSDK.addEditors({
accounts: ['editor-address-1', 'editor-address-2'],
wallet: yourWallet
});
removeEditors(options)
Remove editors from the blog.
const response = await blogSDK.removeEditors({
accounts: ['editor-address-to-remove'],
wallet: yourWallet
});
addAdmins(options)
Add new admins to the blog.
const response = await blogSDK.addAdmins({
accounts: ['admin-address-1', 'admin-address-2'],
wallet: yourWallet
});
removeAdmins(options)
Remove admins from the blog.
const response = await blogSDK.removeAdmins({
accounts: ['admin-address-to-remove'],
wallet: yourWallet
});
getEditors()
Get all current editors.
const response = await blogSDK.getEditors();
getAdmins()
Get all current admins.
const response = await blogSDK.getAdmins();
setBlogDetails(options)
Set blog details (title, description, logo). Admin role required.
const response = await blogSDK.setBlogDetails({
data: {
title: 'My Blog Title',
description: 'My blog description',
logo: 'https://example.com/logo.png'
},
wallet: yourWallet
});
Data Types
BlogPost
interface BlogPost {
id: number;
title: string;
description: string;
body?: string;
published_at: number;
last_update: number;
labels?: string[];
authors: string[];
}
ApiResponse
interface ApiResponse<T = any> {
success: boolean;
data: T | string;
}
CreatePostData
interface CreatePostData {
title: string;
description: string;
body?: string;
published_at: number;
last_update: number;
labels?: string[];
authors: string[];
}
BlogDetails
interface BlogDetails {
title: string;
description: string;
logo: string;
}
BlogInfo
interface BlogInfo {
name: string;
author: string;
blogTitle: string;
blogDescription: string;
blogLogo: string;
details: BlogDetails;
}
UpdateBlogDetailsData
interface UpdateBlogDetailsData {
title?: string;
description?: string;
logo?: string;
}
Deployment
The SDK includes built-in deployment functionality using ao-deploy.
Deploy a New Blog Process
import { InkwellBlogSDK } from '@inkwell.ar/sdk';
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}`);
const blogSDK = new InkwellBlogSDK({
processId: deployResult.processId,
wallet: yourWallet
});
Deployment Options
interface DeployOptions {
name?: string;
wallet?: string | any;
contractPath?: string;
luaPath?: string;
tags?: Array<{ name: string; value: string }>;
retry?: { count: number; delay: number };
minify?: boolean;
contractTransformer?: (source: string) => string;
onBoot?: boolean;
silent?: boolean;
blueprints?: string[];
forceSpawn?: boolean;
}
Examples
Basic Usage
import { InkwellBlogSDK } from '@inkwell.ar/sdk';
const blogSDK = new InkwellBlogSDK({
processId: 'your-process-id'
});
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}`);
}
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
import { InkwellBlogSDK } from '@inkwell.ar/sdk';
import Arweave from 'arweave';
const arweave = Arweave.init();
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:
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']
}
});
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
const rolesResponse = await blogSDK.getUserRoles();
if (rolesResponse.success) {
const roles = rolesResponse.data;
if (roles.includes('EDITOR_ROLE')) {
}
if (roles.includes('DEFAULT_ADMIN_ROLE')) {
}
}
Message Result Retrieval
The SDK automatically attempts to retrieve the actual result of message operations:
- Message Sent: First, the message is sent to the AO process
- Result Retrieval: The SDK then attempts to get the result using the message ID
- 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:
type CreatePostResponse = ApiResponse<BlogPost | string>;
type RoleResponse = ApiResponse<RoleUpdateResult[] | string>;
type BlogDetailsResponse = ApiResponse<BlogDetails | string>;
type DeleteResponse = ApiResponse<string>;
RoleUpdateResult
The RoleUpdateResult type represents the result of role management operations:
type RoleUpdateResult = [string, boolean, string?];
account: The wallet address that was processed
success: Whether the operation succeeded
error: Optional error message if the operation failed
Handling Dual Return Types
const response = await blogSDK.createPost({ data: postData });
if (response.success) {
if (typeof response.data === 'object' && response.data !== null) {
const post = response.data as BlogPost;
console.log(`Post ID: ${post.id}`);
console.log(`Title: ${post.title}`);
} else {
console.log(`Message: ${response.data}`);
}
}
const roleResponse = await blogSDK.addEditors({ accounts: ['address1'], wallet });
if (roleResponse.success) {
if (Array.isArray(roleResponse.data)) {
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 {
console.log(`Message: ${roleResponse.data}`);
}
}
Error Handling
The SDK provides comprehensive error handling:
- Validation Errors: Invalid input data
- Permission Errors: Insufficient role permissions
- Network Errors: Connection issues with AO
- Process Errors: Errors from the AO process itself
- 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
Examples
Basic Usage
See src/examples/basic-usage.ts for a complete example of deploying and using a blog with registry integration.
Registry Usage
See src/examples/basic-registry-usage.ts for examples of:
- Checking permissions for a specific wallet
- Getting user's blogs (admin, editable, all)
- Checking multiple wallets
- Permission checking before actions
- Registry statistics
Aoconnect Deployment
See src/examples/browser-deployment.ts for examples of:
- Deploying blogs using aoconnect
- Using existing blogs
- Cross-environment compatibility
Development
Building
npm run build
Development Mode
npm run dev
Testing
npm test
Linting
npm run lint
Publishing to npm
Before publishing, make sure to:
-
Build the package:
npm run build
-
Test the build:
npm test
-
Check package contents:
npm pack --dry-run
-
Publish to npm:
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