SDK for interacting with the Inkwell Blog CRUD AO process using aoconnect for deployment and interactions
npm install @inkwell.ar/sdkA TypeScript SDK for interacting with the Inkwell Blog CRUD AO process using aoconnect. Published as @inkwell.ar/sdk.
- 🔐 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
``bash`
npm install @inkwell.ar/sdk
Or using yarn:
`bash`
yarn add @inkwell.ar/sdk
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.
The SDK works in browsers for all operations including deployment:
`typescript
import { InkwellBlogSDK, BlogRegistrySDK } from '@inkwell.ar/sdk';
// Deploy a new blog (works in browser!)
// No wallet needed if browser wallet is connected
const result = await InkwellBlogSDK.deploy({
name: 'my-blog'
// wallet: wallet // Optional - will use browser wallet if not provided
});
// Initialize with the deployed blog
const blogSDK = new InkwellBlogSDK({
processId: result.processId
});
// Initialize the registry SDK
const registry = new BlogRegistrySDK();
// Get all posts
const response = await blogSDK.getAllPosts({ ordered: true });
if (response.success) {
console.log('Posts:', response.data);
}
// Check user permissions
const canEdit = await registry.canEditBlog('wallet-address', result.processId);
const canAdmin = await registry.canAdminBlog('wallet-address', result.processId);
// Get user's blogs
const userBlogs = await registry.getWalletBlogs('wallet-address');
`
For Node.js environments (same functionality as browser, uses aoconnect):
#### 1. Deploy the Blog Registry (One-time setup)
`bash`Deploy the registry (requires wallet)
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
`typescript
import { InkwellBlogSDK } from '@inkwell.ar/sdk';
// Deploy a new blog (automatically configured with registry)
// No wallet needed if browser wallet is connected
const result = await InkwellBlogSDK.deploy({
name: 'my-blog'
// wallet: wallet // Optional - will use browser wallet if not provided
});
console.log('Blog deployed:', result.processId);
`
#### 3. Use the Blog and Registry
`typescript
import { InkwellBlogSDK, BlogRegistrySDK } from '@inkwell.ar/sdk';
// Initialize the blog SDK
const blogSDK = new InkwellBlogSDK({
processId: result.processId
});
// Initialize the registry SDK (uses hardcoded registry process ID)
const registry = new BlogRegistrySDK();
// Get all posts
const response = await blogSDK.getAllPosts({ ordered: true });
if (response.success) {
console.log('Posts:', response.data);
}
// Check user permissions
const canEdit = await registry.canEditBlog('wallet-address', result.processId);
const canAdmin = await registry.canAdminBlog('wallet-address', result.processId);
// Get user's blogs
const userBlogs = await registry.getWalletBlogs('wallet-address');
`
`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.
The registry SDK provides read-only access to the centralized permission system:
`typescript
import { BlogRegistrySDK } from '@inkwell.ar/sdk';
const registry = new BlogRegistrySDK();
// Check permissions
const canEdit = await registry.canEditBlog('wallet-address', 'blog-process-id');
const canAdmin = await registry.canAdminBlog('wallet-address', 'blog-process-id');
// Get user's blogs
const userBlogs = await registry.getWalletBlogs('wallet-address');
const adminBlogs = await registry.getAdminBlogs('wallet-address');
const editableBlogs = await registry.getEditableBlogs('wallet-address');
// Get registry statistics
const stats = await registry.getRegistryStats();
`
Security Note: The registry SDK is read-only. Only blog processes can modify permissions in the registry.
#### 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();
#### 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
});
#### 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
});
typescript
interface BlogPost {
id: number;
title: string;
description: string;
body?: string;
published_at: number;
last_update: number;
labels?: string[];
authors: string[];
}
`$3
`typescript
interface ApiResponse {
success: boolean;
data: T | string; // T for successful operations, string for error messages
}
`$3
`typescript
interface CreatePostData {
title: string;
description: string;
body?: string;
published_at: number;
last_update: number;
labels?: string[];
authors: string[];
}
`$3
`typescript
interface BlogDetails {
title: string;
description: string;
logo: string;
}
`$3
`typescript
interface BlogInfo {
name: string;
author: string;
blogTitle: string;
blogDescription: string;
blogLogo: string;
details: BlogDetails;
}
`$3
`typescript
interface UpdateBlogDetailsData {
title?: string;
description?: string;
logo?: string;
}
`Deployment
The SDK includes built-in deployment functionality using ao-deploy.
$3
`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
});
`$3
`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
$3
`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(', ')});
});
}
`$3
`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
});
`$3
`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
$3
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.$3
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)
$3
`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
$3
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
$3
Write operations can return either the actual data or a success message:
`typescript
// createPost and updatePost can return:
type CreatePostResponse = ApiResponse;// addEditors, removeEditors, addAdmins, removeAdmins can return:
type RoleResponse = ApiResponse;
// setBlogDetails can return:
type BlogDetailsResponse = ApiResponse;
// deletePost always returns:
type DeleteResponse = ApiResponse;
`$3
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$3
`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 messageExamples
$3
See
src/examples/basic-usage.ts for a complete example of deploying and using a blog with registry integration.$3
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$3
See
src/examples/browser-deployment.ts for examples of:
- Deploying blogs using aoconnect
- Using existing blogs
- Cross-environment compatibilityDevelopment
$3
`bash
npm run build
`$3
`bash
npm run dev
`$3
`bash
npm test
`$3
`bash
npm run lint
`$3
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 fileThe package excludes:
- Source TypeScript files (
src/`)MIT