@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/core

Core 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.result

Document 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 synchronize
  • docId - 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 docId format 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