@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, openYDoc } 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)

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)
})

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.

import { getAppBus, openYDoc } from '@cloudillo/core'
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.getLoginToken()
await api.auth.getAccessToken({ scope, lifetime })
await api.auth.getAccessTokenByRef(refId)           // Guest access via reference
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)

// ============================================================================
// 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.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.updateStat(actionId, stat)        // Update statistics
await api.actions.addReaction(actionId, { type })   // Add reaction

// ============================================================================
// 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.favorite(fileId)                    // Add to favorites
await api.files.unfavorite(fileId)                  // Remove from favorites
await api.files.listFavorites({ limit })            // List favorites
await api.files.listRecent({ limit })               // List recent files

// ============================================================================
// TRASH - Trash Management
// ============================================================================
await api.trash.list({ limit })                     // List trashed files
await api.trash.empty()                             // Empty trash permanently

// ============================================================================
// COLLECTIONS - Favorites, Bookmarks, Pins
// ============================================================================
await api.collections.list('FAVR', { limit })       // List collection items
await api.collections.add('BKMK', itemId)           // Add to collection
await api.collections.remove('PIND', itemId)        // Remove from collection
// Collection types: 'FAVR' | 'RCNT' | 'BKMK' | 'PIND'

// ============================================================================
// 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

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