@cloudillo/core
Overview
The @cloudillo/core library is the core SDK for Cloudillo applications. It provides initialization via the message bus, API client creation, CRDT document support, and URL helpers.
Installation
pnpm add @cloudillo/coreCore Pattern: AppMessageBus
The main API is accessed through the getAppBus() singleton. This provides authentication state, storage, and shell communication for apps running in the Cloudillo shell.
import { getAppBus } from '@cloudillo/core'
// Get the singleton message bus
const bus = getAppBus()
// Initialize your app (communicates with shell)
const state = await bus.init('my-app')
// Access state via bus properties
console.log('Token:', bus.accessToken)
console.log('User ID:', bus.idTag)
console.log('Tenant ID:', bus.tnId)
console.log('Roles:', bus.roles)
console.log('Access level:', bus.access)
console.log('Dark mode:', bus.darkMode)AppState Properties
After calling bus.init(), these properties are available on the bus:
| Property | Type | Description |
|---|---|---|
accessToken |
string | undefined |
Current JWT access token |
idTag |
string | undefined |
User’s identity tag (e.g., “alice.cloudillo.net”) |
tnId |
number | undefined |
Tenant ID |
roles |
string[] | undefined |
User’s roles |
access |
'read' | 'write' |
Access level to current resource |
darkMode |
boolean |
Dark mode preference |
tokenLifetime |
number | undefined |
Token lifetime in seconds |
displayName |
string | undefined |
Display name (for anonymous guests) |
embedded |
boolean |
Whether the app is running as an embedded document |
theme |
string | undefined |
Theme name |
navState |
string | undefined |
Initial navigation state (from parent embed or shell) |
ancestors |
string[] | undefined |
Ancestor file IDs in the embed chain |
Storage API
The bus provides namespaced key-value storage:
const bus = getAppBus()
await bus.init('my-app')
// Store data
await bus.storage.set('my-app', 'settings', { theme: 'dark' })
// Retrieve data
const settings = await bus.storage.get<{ theme: string }>('my-app', 'settings')
// List keys
const keys = await bus.storage.list('my-app', 'user-')
// Delete data
await bus.storage.delete('my-app', 'settings')
// Clear namespace
await bus.storage.clear('my-app')
// Check quota
const quota = await bus.storage.quota('my-app')
console.log(`Used ${quota.used} of ${quota.limit} bytes`)App Lifecycle Notifications
Notify the shell about your app’s loading progress:
const bus = getAppBus()
await bus.init('my-app')
// After auth init (called automatically by init())
bus.notifyReady('auth')
// After CRDT sync complete
bus.notifyReady('synced')
// When fully interactive
bus.notifyReady('ready')Token Refresh
Request a fresh token when needed:
const bus = getAppBus()
// Manually refresh token
const newToken = await bus.refreshToken()
// Listen for token updates pushed from shell
bus.on('auth:token.push', (msg) => {
console.log('Token updated:', bus.accessToken)
})Error Notification
Notify the shell about errors in your app:
bus.notifyError(404, 'Document not found')Event Handling
Register and unregister message handlers:
bus.on('auth:token.push', handler)
bus.off('auth:token.push', handler)Media Picker
Open the shell’s media picker dialog:
const result = await bus.pickMedia({
mediaType: 'image/*', // MIME filter
enableCrop: true, // Enable crop UI
cropAspects: ['16:9', '1:1'], // Allowed aspect ratios
documentFileId: 'abc123', // Context document
title: 'Choose image'
})
// Returns: { fileId, fileName, contentType, dim?, visibility?, croppedVariantId? }
Document Picker
Open the shell’s document picker:
const result = await bus.pickDocument({
fileTp: 'CRDT', // File type filter
contentType: 'cloudillo/quillo', // Content type filter
sourceFileId: 'abc123', // Source context
title: 'Choose document'
})
// Returns: { fileId, fileName, contentType, fileTp?, appId? }
Camera Capture
Capture an image from the device camera:
const result = await bus.captureImage({
facing: 'environment', // 'user' or 'environment'
maxResolution: 1920
})
// Returns: { imageData (base64), width, height }
Camera Preview
Open a camera preview with overlay support:
const session = await bus.openCamera({ facing: 'environment' })
// Preview frames
bus.previewFrames(session.sessionId, (frameData) => { /* ... */ })
// Add overlay shapes
bus.overlayShapes(session.sessionId, shapes)
// Wait for capture
const result = await session.resultDocument Embedding
Request an embedded document view:
const { embedUrl, nonce, resId } = await bus.requestEmbed({
targetFileId: 'abc123',
targetContentType: 'cloudillo/quillo',
sourceFileId: 'current-doc',
access: 'read',
navState: 'page=3'
})Settings API
Access user settings through the bus:
await bus.settings.get('key')
await bus.settings.set('key', value)
const items = await bus.settings.list('prefix')CRDT Cache Management
Manage offline CRDT caching:
const clientId = await bus.requestClientId(docId)
await bus.crdtCacheAppend(docId, update, clientId, clock)
const updates = await bus.crdtCacheRead(docId)
await bus.crdtCacheCompact(docId, state)resetAppBus()
Reset the singleton message bus instance (useful for testing):
import { resetAppBus } from '@cloudillo/core'
resetAppBus()API Client
createApiClient(opts: ApiClientOpts): ApiClient
Create a type-safe REST API client.
import { createApiClient } from '@cloudillo/core'
const api = createApiClient({
idTag: 'alice.cloudillo.net', // Required: target tenant
authToken: 'jwt-token' // Optional: authentication token
})
// Use the API
const profile = await api.profiles.getOwn()
const posts = await api.actions.list({ type: 'POST', limit: 20 })Options:
interface ApiClientOpts {
idTag: string // Required: identity tag of the tenant
authToken?: string // Optional: JWT token for authentication
}idTag is Required
Unlike the old documentation, idTag is required to create an API client. This specifies which Cloudillo instance to connect to.
Using with AppMessageBus
import { getAppBus, createApiClient } from '@cloudillo/core'
const bus = getAppBus()
const state = await bus.init('my-app')
const api = createApiClient({
idTag: bus.idTag!,
authToken: bus.accessToken
})
const files = await api.files.list({ limit: 20 })CRDT Document Functions
openYDoc(yDoc: Y.Doc, docId: string): Promise<DocConnection>
Open a Yjs document for collaborative editing with WebSocket synchronization.
Info
openYDoc is exported from @cloudillo/crdt, not from @cloudillo/core.
import { getAppBus } from '@cloudillo/core'
import { openYDoc } from '@cloudillo/crdt'
import * as Y from 'yjs'
const bus = getAppBus()
await bus.init('my-app')
const yDoc = new Y.Doc()
const { yDoc: doc, provider } = await openYDoc(yDoc, 'alice.cloudillo.net:document-id')
// Use shared types
const yText = yDoc.getText('content')
yText.insert(0, 'Hello, collaborative world!')
// Listen for changes
yText.observe(() => {
console.log('Text changed:', yText.toString())
})
// Access awareness (other users' cursors, selections)
provider.awareness.on('change', () => {
const states = provider.awareness.getStates()
console.log('Connected users:', states.size)
})Parameters:
yDoc- The Yjs document to synchronizedocId- Document ID in format"targetTag:resourceId"
Returns:
interface DocConnection {
yDoc: Y.Doc
provider: WebsocketProvider
}Error Handling:
- Throws if no access token (must call
init()first) - Throws if
docIdformat is invalid - WebSocket close codes 4401/4403/4404 stop reconnection (auth/permission/not found errors)
URL Helper Functions
Build URLs for Cloudillo services:
getInstanceUrl(idTag: string): string
Build the base URL for a Cloudillo instance.
import { getInstanceUrl } from '@cloudillo/core'
const url = getInstanceUrl('alice.cloudillo.net')
// Returns: "https://cl-o.alice.cloudillo.net"
getApiUrl(idTag: string): string
Build the API base URL.
import { getApiUrl } from '@cloudillo/core'
const url = getApiUrl('alice.cloudillo.net')
// Returns: "https://cl-o.alice.cloudillo.net/api"
getFileUrl(idTag: string, fileId: string, variant?: string): string
Build URL for file access with optional variant.
import { getFileUrl } from '@cloudillo/core'
// Basic file URL
const url = getFileUrl('alice.cloudillo.net', 'file-123')
// Returns: "https://cl-o.alice.cloudillo.net/api/files/file-123"
// With variant
const thumbnailUrl = getFileUrl('alice.cloudillo.net', 'file-123', 'vis.tn')
// Returns: "https://cl-o.alice.cloudillo.net/api/files/file-123?variant=vis.tn"
getCrdtUrl(idTag: string): string
Build the CRDT WebSocket URL.
import { getCrdtUrl } from '@cloudillo/core'
const url = getCrdtUrl('alice.cloudillo.net')
// Returns: "wss://cl-o.alice.cloudillo.net/ws/crdt"
getRtdbUrl(idTag: string, fileId: string, token: string): string
Build the RTDB WebSocket URL with authentication.
import { getRtdbUrl } from '@cloudillo/core'
const url = getRtdbUrl('alice.cloudillo.net', 'db-file-id', 'jwt-token')
// Returns: "wss://cl-o.alice.cloudillo.net/ws/rtdb/db-file-id?token=jwt-token"
getMessageBusUrl(idTag: string, token: string): string
Build the Message Bus WebSocket URL.
import { getMessageBusUrl } from '@cloudillo/core'
const url = getMessageBusUrl('alice.cloudillo.net', 'jwt-token')
// Returns: "wss://cl-o.alice.cloudillo.net/ws/bus?token=jwt-token"
Image/Video Variant Helpers
getOptimalImageVariant(context, localVariants?): string
Get the optimal image variant for a display context.
import { getOptimalImageVariant } from '@cloudillo/core'
// For thumbnail display
const variant = getOptimalImageVariant('thumbnail') // 'vis.tn'
// For preview
const variant = getOptimalImageVariant('preview') // 'vis.sd'
// For fullscreen/lightbox
const variant = getOptimalImageVariant('fullscreen') // 'vis.hd'
// With available variants
const variant = getOptimalImageVariant('fullscreen', ['vis.sd', 'vis.hd', 'vis.xd'])
// Returns highest quality available: 'vis.xd'
Image variants: vis.tn (150px), vis.sd (640px), vis.md (1280px), vis.hd (1920px), vis.xd (original)
getOptimalVideoVariant(context, localVariants?): string
Get the optimal video variant for a display context.
import { getOptimalVideoVariant } from '@cloudillo/core'
const variant = getOptimalVideoVariant('preview') // 'vid.sd'
const variant = getOptimalVideoVariant('fullscreen') // 'vid.hd'
Video variants: vid.sd, vid.md, vid.hd, vid.xd
getImageVariantForDisplaySize(width, height): string
Get optimal variant for a specific display size in pixels.
import { getImageVariantForDisplaySize } from '@cloudillo/core'
// For a 300x200 canvas at 2x zoom (600x400 screen pixels)
const variant = getImageVariantForDisplaySize(600, 400)
// Returns: 'vis.sd'
API Client Structure
The API client provides access to all REST endpoints organized by namespace:
const api = createApiClient({ idTag: 'alice.cloudillo.net', authToken: token })
// ============================================================================
// AUTH - Authentication & Authorization
// ============================================================================
await api.auth.login({ idTag, password })
await api.auth.logout()
await api.auth.loginInit() // Combined init (auth or QR+WebAuthn)
await api.auth.getLoginToken()
await api.auth.getAccessToken({ scope, lifetime })
await api.auth.getAccessTokenByRef(refId) // Guest access via reference
await api.auth.getAccessTokenVia(via, scope) // Scoped token via cross-document link
await api.auth.getAccessTokenByApiKey(apiKey) // API key authentication
await api.auth.getProxyToken(targetIdTag) // Federation proxy token
await api.auth.getVapidPublicKey() // Push notification key
await api.auth.changePassword({ oldPassword, newPassword })
await api.auth.setPassword({ refId, password }) // Set password via reset link
await api.auth.forgotPassword({ email })
// WebAuthn
await api.auth.listWebAuthnCredentials()
await api.auth.getWebAuthnRegChallenge()
await api.auth.registerWebAuthnCredential({ token, credential })
await api.auth.deleteWebAuthnCredential(credentialId)
await api.auth.getWebAuthnLoginChallenge()
await api.auth.webAuthnLogin({ token, credential })
// API Keys
await api.auth.listApiKeys()
await api.auth.createApiKey({ name, scopes })
await api.auth.deleteApiKey(keyId)
// QR Login
await api.auth.initQrLogin() // Start QR login session
await api.auth.getQrLoginStatus(sessionId, secret) // Poll for login status
await api.auth.getQrLoginDetails(sessionId) // Get session details (mobile)
await api.auth.respondQrLogin(sessionId, data) // Approve/deny from mobile
// ============================================================================
// PROFILE - Registration & Profile Creation
// ============================================================================
await api.profile.verify({ type, idTag, email }) // Check availability
await api.profile.register({ type, idTag, name, email, password })
// ============================================================================
// PROFILES - Profile Management
// ============================================================================
await api.profiles.getOwn() // GET /me
await api.profiles.getOwnFull() // GET /me/full
await api.profiles.getRemoteFull(idTag) // GET full profile from remote server
await api.profiles.updateOwn({ name, bio }) // PATCH /me
await api.profiles.list({ q, type, role }) // List/search profiles
await api.profiles.get(idTag) // Get profile by idTag
await api.profiles.updateConnection(idTag, data) // Update relationship
await api.profiles.adminUpdate(idTag, { status, roles }) // Admin operations
// ============================================================================
// ACTIONS - Social Interactions
// ============================================================================
await api.actions.list({ type, status, limit }) // List actions
await api.actions.listPaginated({ cursor, limit }) // With cursor pagination
await api.actions.create({ type, content, ... }) // Create action
await api.actions.get(actionId) // Get single action
await api.actions.update(actionId, patch) // Update draft
await api.actions.delete(actionId) // Delete action
await api.actions.accept(actionId) // Accept (e.g., connection)
await api.actions.reject(actionId) // Reject
await api.actions.dismiss(actionId) // Dismiss notification
await api.actions.updateStat(actionId, stat) // Update statistics
await api.actions.addReaction(actionId, { type }) // Add reaction
await api.actions.publish(actionId, { publishAt }) // Publish draft (optionally scheduled)
await api.actions.cancel(actionId) // Cancel scheduled (revert to draft)
// ============================================================================
// FILES - File Management
// ============================================================================
await api.files.list({ parentId, tag, limit }) // List files
await api.files.listPaginated({ cursor, limit }) // With cursor pagination
await api.files.create({ fileName, fileTp }) // Create metadata-only file
await api.files.uploadBlob(preset, name, data, contentType) // Upload file
await api.files.get(fileId) // Get file content
await api.files.getVariant(variantId) // Get specific variant
await api.files.getDescriptor(fileId) // Get file metadata
await api.files.update(fileId, { fileName }) // Update metadata
await api.files.delete(fileId) // Soft delete (to trash)
await api.files.permanentDelete(fileId) // Permanent delete
await api.files.restore(fileId, parentId) // Restore from trash
await api.files.updateUserData(fileId, { starred }) // Update user-specific data
await api.files.setStarred(fileId, true) // Toggle starred
await api.files.setPinned(fileId, true) // Toggle pinned
await api.files.addTag(fileId, 'tag') // Add tag
await api.files.removeTag(fileId, 'tag') // Remove tag
await api.files.duplicate(fileId, { fileName }) // Duplicate a CRDT/RTDB file
await api.files.listShares(fileId) // List share entries for file
await api.files.createShare(fileId, data) // Create share entry
await api.files.deleteShare(fileId, shareId) // Delete share entry
// ============================================================================
// SHARES - Share Entry Queries
// ============================================================================
await api.shares.listBySubject(subjectId, subjectType) // List shares by subject
// ============================================================================
// TRASH - Trash Management
// ============================================================================
await api.trash.list({ limit }) // List trashed files
await api.trash.empty() // Empty trash permanently
// ============================================================================
// TAGS - Tag Management
// ============================================================================
await api.tags.list({ prefix, withCounts }) // List tags
// ============================================================================
// SETTINGS - User Settings
// ============================================================================
await api.settings.list({ prefix }) // List settings
await api.settings.get(name) // Get setting value
await api.settings.update(name, { value }) // Update setting
// ============================================================================
// NOTIFICATIONS - Push Notifications
// ============================================================================
await api.notifications.subscribe({ subscription }) // Subscribe to push
// ============================================================================
// REFS - Share Links & References
// ============================================================================
await api.refs.list({ type, resourceId }) // List references
await api.refs.get(refId) // Get reference
await api.refs.create({ type, resourceId, access }) // Create share link
await api.refs.delete(refId) // Delete reference
// ============================================================================
// IDP - Identity Provider (End User)
// ============================================================================
await api.idp.getInfo(providerDomain) // Get provider info
await api.idp.activate({ refId }) // Activate identity
// ============================================================================
// IDP MANAGEMENT - Identity Provider Admin
// ============================================================================
await api.idpManagement.listIdentities({ q, status })
await api.idpManagement.createIdentity({ idTag, email, createApiKey })
await api.idpManagement.getIdentity(idTag)
await api.idpManagement.updateIdentity(idTag, { dyndns })
await api.idpManagement.deleteIdentity(idTag)
await api.idpManagement.listApiKeys(idTag)
await api.idpManagement.createApiKey({ idTag, name })
await api.idpManagement.deleteApiKey(keyId, idTag)
// ============================================================================
// COMMUNITIES - Community Management
// ============================================================================
await api.communities.create(idTag, { type, name, ownerIdTag })
await api.communities.verify({ type, idTag }) // Deprecated: use profile.verify
// ============================================================================
// ADMIN - System Administration
// ============================================================================
await api.admin.listTenants({ q, status, limit }) // List all tenants
await api.admin.sendPasswordReset(idTag) // Send password reset
await api.admin.sendTestEmail(to) // Test SMTP config
await api.admin.listProxySites() // List proxy site configs
await api.admin.createProxySite(data) // Create proxy site
await api.admin.getProxySite(siteId) // Get proxy site
await api.admin.updateProxySite(siteId, data) // Update proxy site
await api.admin.deleteProxySite(siteId) // Delete proxy site
await api.admin.renewProxySiteCert(siteId) // Renew proxy site TLS cert
Helper Functions
delay(ms: number): Promise<void>
Delay execution for a specified time.
import { delay } from '@cloudillo/core'
await delay(1000) // Wait 1 second
See Also
- @cloudillo/react - React integration with hooks
- @cloudillo/rtdb - Real-time database
- REST API Reference - Complete API documentation