@crit-fumble/core
v10.35.0
Published
Client SDK for Crit-Fumble Core API - shared types and HTTP client
Maintainers
Readme
@crit-fumble/core
Official TypeScript SDK for Crit-Fumble Core API. Provides type-safe HTTP client, dice rolling utilities, and shared types for tabletop RPG applications.
Installation
npm install @crit-fumble/coreFeatures
- 🎲 Dice Rolling - Advanced dice notation parser with exploding dice, advantage/disadvantage, and more
- 🔐 Authentication - OAuth integration with Discord and GitHub
- 📚 Knowledge Base - Access 360+ game system articles and rules references
- 🎮 Campaign Management - Players, sessions, and message logging
- 🗺️ CFG-X Adventure Engine - Text-based RPG with World Anvil lore integration
- 🎯 Type Safety - Full TypeScript support with comprehensive type definitions
- ⚡ Modern - Built for ES2022+ with native ESM
Quick Start
Basic API Client
import { CoreApiClient } from '@crit-fumble/core'
const api = new CoreApiClient({
baseUrl: process.env.CORE_API_URL || 'https://core.crit-fumble.com',
apiKey: process.env.CORE_API_KEY
})
// Health check
const health = await api.health()
console.log(health.status) // 'ok'Dice Rolling (v10.4.0+)
import { rollDice, validateDiceNotation } from '@crit-fumble/core'
// Basic rolls
const attack = rollDice('1d20+5')
console.log(attack.total) // 17
console.log(attack.rolls) // [12]
console.log(attack.modifier) // 5
// Advantage (D&D 5e)
const advantage = rollDice('2d20kh1+3')
console.log(advantage.output) // "2d20kh1+3: [15d, 18]+3 = 21"
// Ability score generation
const stats = rollDice('4d6d1') // Drop lowest
console.log(stats.total) // 14
// Exploding dice (Savage Worlds)
const damage = rollDice('3d6!')
console.log(damage.output) // "3d6!: [6!, 4, 5, 2] = 17"
// Validation
console.log(validateDiceNotation('2d6+3')) // true
console.log(validateDiceNotation('invalid')) // falseSupported Dice Notation
| Notation | Description | Example Output |
|----------|-------------|----------------|
| 2d6+3 | Basic roll with modifier | [4, 5]+3 = 12 |
| 4d6d1 | Drop lowest (ability scores) | [2d, 5, 6, 4] = 15 |
| 4d6dh1 | Drop highest | [6d, 3, 4, 2] = 9 |
| 4d6k3 | Keep 3 highest | [5, 6, 4, 2d] = 15 |
| 2d20kh1 | Advantage (keep highest) | [12d, 17] = 17 |
| 2d20kl1 | Disadvantage (keep lowest) | [8, 15d] = 8 |
| 3d6! | Exploding dice | [6!, 5, 4, 1] = 16 |
| 2d10r1 | Reroll 1s once | [1r→7, 5] = 12 |
| 1d100 | Percentile | [73] = 73 |
See Dice Notation Guide for full documentation.
API Documentation
Knowledge Base
Access game system documentation, rules, and references:
// List articles by system
const { articles } = await api.kb.list({ system: 'dnd5e' })
// Search articles
const { articles } = await api.kb.list({ search: 'dice notation' })
// Get specific article
const { article } = await api.kb.get('common/dice-notation')
console.log(article.frontmatter.title) // "Dice Notation Guide"
console.log(article.content) // Full markdown content
// Get available systems
const { systems } = await api.kb.getSystems()
// Returns: ['common', 'dnd5e', 'pf2e', 'cypher', 'foundry']Campaign Management
Manage campaigns with players, sessions, and messages:
// List campaigns for a guild
const { campaigns } = await api.campaigns.list({ guildId: 'guild_123' })
// Create campaign
const { campaign } = await api.campaigns.create({
name: 'Lost Mine of Phandelver',
system: 'dnd5e',
guildId: 'guild_123',
status: 'active'
})
// Add player
const { player } = await api.players.add(campaign.id, {
userId: 'user_456',
role: 'player',
metadata: { characterName: 'Gandalf' }
})
// Create session
const { session } = await api.sessions.create(campaign.id, {
scheduledAt: new Date(),
metadata: { location: 'voice-channel-789' }
})
// Log message
const { message } = await api.messages.log(session.id, {
discordId: 'user_456',
content: 'I cast fireball!',
messageType: 'ic', // in-character
metadata: { diceRoll: '8d6' }
})Dice Roll Logging
Log dice rolls with full context for statistics and history:
// Log a dice roll (requires service auth)
const { roll } = await api.dice.log({
notation: '2d20kh1+5',
rolls: [12, 18],
modifier: 5,
total: 23,
isCrit: false,
isFumble: false,
label: 'Attack Roll',
discordId: 'user_123',
guildId: 'guild_456',
channelId: 'channel_789',
source: 'discord'
})
// Get user statistics
const { stats } = await api.dice.getStats('user_123', {
guildId: 'guild_456',
limit: 10
})
console.log(stats.critRate) // 0.05 (5%)
console.log(stats.recentRolls) // Last 10 rollsContainer Management (v10.14.0+)
Manage Docker containers (Foundry, SteamCMD, Adventure Terminal):
// List containers
const { containers, total } = await api.container.list({
type: 'foundry',
status: 'running'
})
// Create container
const container = await api.container.create({
type: 'foundry',
userId: 'user_123',
imageTag: 'felddy/foundryvtt:release',
config: { license: 'ABC-123' }
})
// Start/Stop container
await api.container.start(container.id)
await api.container.stop(container.id)
// Get container stats (CPU, memory, network)
const stats = await api.container.stats(container.id)
console.log(`CPU: ${stats.cpu.percent}%, Memory: ${stats.memory.percent}%`)
// Execute command in container
const result = await api.container.exec(container.id, {
command: ['ls', '-la', '/data']
})
console.log(result.stdout)
// File operations
const fileContent = await api.container.readFile(container.id, '/data/world.json')
await api.container.writeFile(container.id, '/data/config.json', {
content: JSON.stringify(config),
encoding: 'utf-8'
})
await api.container.deleteFile(container.id, '/data/old-file.txt')
// Container logs
const logs = await api.container.logs(container.id, { level: 'error', limit: 50 })
const dockerLogs = await api.container.dockerLogs(container.id, { tail: 100 })
// Templates
const { templates } = await api.container.listTemplates({ type: 'foundry' })
const newContainer = await api.container.createFromTemplate({
templateId: 'foundry-dnd5e',
overrides: { name: 'My Campaign Server' }
})
// Shared containers (Adventure Terminal)
const terminal = await api.container.getSharedAdventureTerminal()
const execResult = await api.container.execInSharedTerminal(['echo', 'Hello'])FoundryVTT Integration
Manage FoundryVTT licenses and VTT container content:
// License management
const { licenses } = await api.foundry.getLicenses()
await api.foundry.linkLicense({
licenseKey: 'FOUNDRY-LICENSE-KEY',
ownerName: 'John Doe',
email: '[email protected]'
})
await api.foundry.unlinkLicense('license_id')
// Container management
const { container } = await api.foundry.createContainer({
guildId: 'guild_123',
foundryVersion: '12',
worldName: 'My Campaign'
})
const { containers } = await api.foundry.listContainers()
await api.foundry.stopContainer(container.containerId)
// VTT World management (v10.14.0+)
const { worlds } = await api.foundry.listWorlds(containerId)
const { world } = await api.foundry.getWorld(containerId, 'world-id')
await api.foundry.backupWorld(containerId, 'world-id')
// Module management
const { modules } = await api.foundry.listModules(containerId)
await api.foundry.installModule(containerId, 'https://manifest-url/module.json')
await api.foundry.uninstallModule(containerId, 'module-id')
// Game systems
const { systems } = await api.foundry.listSystems(containerId)
// Settings
const { settings } = await api.foundry.getSettings(containerId)
await api.foundry.updateSettings(containerId, { 'core.language': 'en' })
// Admin & Users
await api.foundry.setAdminPassword(containerId, 'new-password')
const { users } = await api.foundry.listUsers(containerId)
// License pool (admin)
const stats = await api.foundry.getPoolStats()
const { licenses } = await api.foundry.listPoolLicenses()
await api.foundry.addPoolLicense({ licenseKey: '...', notes: 'Pool license' })Authentication
The SDK supports multiple authentication methods:
API Key (Service-to-Service)
const api = new CoreApiClient({
baseUrl: 'https://core.crit-fumble.com',
apiKey: process.env.CORE_API_KEY // X-API-Key header
})Service Secret (Internal Services)
For authenticated services like FumbleBot:
const api = new CoreApiClient({
baseUrl: 'https://core.crit-fumble.com',
serviceSecret: process.env.CORE_SECRET, // X-Core-Secret header
getUserContext: () => ({
userId: currentUser.id,
guildId: currentGuild.id,
channelId: currentChannel.id
})
})Session Cookie (Web Apps)
For browser-based applications:
const api = new CoreApiClient({
baseUrl: 'https://core.crit-fumble.com',
credentials: 'include' // Send cookies
})
// Check auth status
const { user } = await api.auth.me()
if (user) {
console.log(`Logged in as ${user.name}`)
}Multi-Provider OAuth (v10.15.0+)
Core supports multiple OAuth providers. Users can link multiple accounts and sign in with any of them:
// Get available OAuth providers
const { providers } = await api.auth.providers()
// Returns: [{ id: 'discord', name: 'Discord', signinUrl: '...', ... }, ...]
// Display sign-in options
for (const provider of providers) {
console.log(`Sign in with ${provider.name}: ${provider.signinUrl}`)
}
// Get user's linked accounts
const { accounts } = await api.auth.linkedAccounts()
for (const account of accounts) {
console.log(`${account.provider}: ${account.providerAccountId}`)
}
// Output: discord: 123456789, github: octocat, twitch: ninjaSupported providers:
- Discord (required) - Primary auth provider
- GitHub (optional) - Enable with
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET - Twitch (optional) - Enable with
TWITCH_CLIENT_ID/TWITCH_CLIENT_SECRET
Account linking: When a user signs in with a new provider that has the same email as an existing account, Auth.js automatically links them. Users can then sign in with any linked provider.
Web Application Auth Proxy Setup
For Next.js applications that need to proxy authenticated requests to Core API.
1. Configure API Routes (App Router)
Create a catch-all API route to proxy requests:
// app/api/core/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server'
const CORE_API_URL = process.env.CORE_API_URL || 'https://core.crit-fumble.com'
const CORE_SECRET = process.env.CORE_SECRET
async function proxyRequest(req: NextRequest, path: string) {
const url = `${CORE_API_URL}${path}`
// Forward cookies for session auth
const headers = new Headers(req.headers)
headers.set('X-Core-Secret', CORE_SECRET || '')
const response = await fetch(url, {
method: req.method,
headers,
body: ['GET', 'HEAD'].includes(req.method) ? undefined : await req.text(),
})
return new NextResponse(response.body, {
status: response.status,
headers: response.headers,
})
}
export async function GET(req: NextRequest, { params }: { params: { path: string[] } }) {
const path = '/' + params.path.join('/')
return proxyRequest(req, `/api${path}${req.nextUrl.search}`)
}
export async function POST(req: NextRequest, { params }: { params: { path: string[] } }) {
const path = '/' + params.path.join('/')
return proxyRequest(req, `/api${path}`)
}
export async function PUT(req: NextRequest, { params }: { params: { path: string[] } }) {
const path = '/' + params.path.join('/')
return proxyRequest(req, `/api${path}`)
}
export async function PATCH(req: NextRequest, { params }: { params: { path: string[] } }) {
const path = '/' + params.path.join('/')
return proxyRequest(req, `/api${path}`)
}
export async function DELETE(req: NextRequest, { params }: { params: { path: string[] } }) {
const path = '/' + params.path.join('/')
return proxyRequest(req, `/api${path}`)
}2. Configure SDK Client
// lib/core-api.ts
import { CoreApiClient } from '@crit-fumble/core'
// Server-side: Direct connection with service auth
export const coreApiServer = new CoreApiClient({
baseUrl: process.env.CORE_API_URL || 'https://core.crit-fumble.com',
serviceSecret: process.env.CORE_SECRET,
})
// Client-side: Proxy through Next.js API routes
export const coreApiClient = new CoreApiClient({
baseUrl: '/api/core', // Proxied through Next.js
credentials: 'include',
})3. Auth.js Adapter Integration
For SSO with Core's Auth.js database adapter:
// lib/auth.ts
import NextAuth from 'next-auth'
import Discord from 'next-auth/providers/discord'
import { CoreApiClient } from '@crit-fumble/core'
const coreApi = new CoreApiClient({
baseUrl: process.env.CORE_API_URL!,
serviceSecret: process.env.CORE_SECRET!,
})
// Custom adapter that proxies to Core
const CoreAdapter = {
async createUser(user) {
return coreApi.authAdapter.createUser(user)
},
async getUser(id) {
return coreApi.authAdapter.getUser(id)
},
async getUserByEmail(email) {
return coreApi.authAdapter.getUserByEmail(email)
},
async getUserByAccount({ provider, providerAccountId }) {
return coreApi.authAdapter.getUserByAccount(provider, providerAccountId)
},
async updateUser(user) {
return coreApi.authAdapter.updateUser(user.id, user)
},
async linkAccount(account) {
return coreApi.authAdapter.linkAccount(account)
},
async unlinkAccount({ provider, providerAccountId }) {
return coreApi.authAdapter.unlinkAccount(provider, providerAccountId)
},
async createSession(session) {
return coreApi.authAdapter.createSession(session)
},
async getSessionAndUser(sessionToken) {
return coreApi.authAdapter.getSessionAndUser(sessionToken)
},
async updateSession(session) {
return coreApi.authAdapter.updateSession(session.sessionToken, session)
},
async deleteSession(sessionToken) {
return coreApi.authAdapter.deleteSession(sessionToken)
},
}
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: CoreAdapter,
providers: [
Discord({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
}),
],
})4. Environment Variables
# Web App (.env.local)
CORE_API_URL=https://core.crit-fumble.com
CORE_SECRET=your-service-secret
# Discord OAuth
DISCORD_CLIENT_ID=your-client-id
DISCORD_CLIENT_SECRET=your-client-secret
# Auth.js
AUTH_SECRET=random-secret-for-auth-jsDiscord Activity Integration
For Discord Activities using the Embedded App SDK:
import { CoreApiClient } from '@crit-fumble/core'
import { DiscordSDK } from '@discord/embedded-app-sdk'
const discordSdk = new DiscordSDK(process.env.DISCORD_CLIENT_ID!)
// Initialize SDK
await discordSdk.ready()
// Create API client with Discord context
const api = new CoreApiClient({
baseUrl: '/.proxy', // Proxied through your activity server
getDiscordContext: () => ({
guildId: discordSdk.guildId,
channelId: discordSdk.channelId,
instanceId: discordSdk.instanceId
})
})
// Start container for terminal activity
const { container } = await api.container.start({
guildId: discordSdk.guildId!,
channelId: discordSdk.channelId!
})
// Connect WebSocket terminal
const ws = new WebSocket(container.wsUrl)
ws.onmessage = (event) => {
const { type, content } = JSON.parse(event.data)
console.log(`[${type}]`, content)
}
// Send command
ws.send(JSON.stringify({
type: 'command',
content: 'roll 2d6+3'
}))Implementation Guides
FumbleBot (Discord Bot / AI Assistant)
FumbleBot uses the SDK to interact with Core as the command layer for voice, dice, campaigns, and AI assistance.
1. Service Authentication
import { CoreApiClient } from '@crit-fumble/core'
const api = new CoreApiClient({
baseUrl: process.env.CORE_API_URL || 'https://core.crit-fumble.com',
serviceSecret: process.env.CORE_SECRET, // X-Core-Secret header
})2. Voice Command WebSocket (Real-time)
Connect to Core's WebSocket to receive voice commands in real-time:
import WebSocket from 'ws'
import type { VoiceServerMessage, VoiceClientMessage } from '@crit-fumble/core'
const CORE_WS_URL = process.env.CORE_WS_URL || 'wss://core.crit-fumble.com'
// Connect with service auth
const ws = new WebSocket(
`${CORE_WS_URL}/api/voice/ws?secret=${process.env.CORE_SECRET}&serviceId=fumblebot`
)
ws.on('open', () => {
console.log('Connected to Core Voice WebSocket')
// Subscribe to all guilds FumbleBot is in
const guildIds = client.guilds.cache.map(g => g.id)
ws.send(JSON.stringify({
type: 'subscribe',
guilds: guildIds
} satisfies VoiceClientMessage))
})
ws.on('message', async (data) => {
const message: VoiceServerMessage = JSON.parse(data.toString())
switch (message.type) {
case 'connected':
console.log(`Connected as ${message.serviceId}`)
break
case 'subscribed':
console.log(`Subscribed to ${message.guilds.length} guilds`)
break
case 'command':
await handleVoiceCommand(message.command)
break
case 'pong':
// Heartbeat response
break
}
})
// Handle voice commands from Activities/Web
async function handleVoiceCommand(command: VoiceCommand) {
const { id, guildId, type, payload } = command
try {
switch (type) {
case 'join':
const channel = await client.channels.fetch(payload.channelId as string)
await joinVoiceChannel({ channel, ... })
break
case 'leave':
const connection = getVoiceConnection(guildId)
connection?.destroy()
break
case 'play':
await playAudio(guildId, payload.url as string, payload.volume as number)
break
case 'listen_start':
await startListening(guildId, payload.channelId as string)
break
}
// Acknowledge success
ws.send(JSON.stringify({
type: 'ack',
commandId: id,
status: 'completed'
} satisfies VoiceClientMessage))
} catch (error) {
// Acknowledge failure
ws.send(JSON.stringify({
type: 'ack',
commandId: id,
status: 'failed'
} satisfies VoiceClientMessage))
}
}
// Keep connection alive
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30000)3. Voice Session Reporting
Report voice state changes to Core for Activities/Web visibility:
import { Client, VoiceState } from 'discord.js'
client.on('voiceStateUpdate', async (oldState, newState) => {
// FumbleBot joined a channel
if (newState.member?.id === client.user?.id && newState.channel) {
await api.voice.updateSession({
guildId: newState.guild.id,
channelId: newState.channel.id,
channelName: newState.channel.name,
participants: newState.channel.members.map(m => m.id),
listening: false,
playing: false
})
}
// FumbleBot left a channel
if (oldState.member?.id === client.user?.id && !newState.channel) {
await api.voice.endSession({ guildId: oldState.guild.id })
}
})
// Update listening/playing state
async function updateVoiceState(guildId: string, updates: Partial<VoiceSessionUpdateRequest>) {
await api.voice.updateSession({
guildId,
...updates
})
}4. Voice Command Polling (Alternative)
If WebSocket isn't suitable, poll for commands:
async function pollVoiceCommands(guildId: string) {
const { commands } = await api.voice.getCommands({ guildId })
for (const command of commands) {
await handleVoiceCommand(command)
await api.voice.ackCommand({
commandId: command.id,
status: 'completed'
})
}
}
// Poll every 5 seconds
setInterval(() => {
for (const guild of client.guilds.cache.values()) {
pollVoiceCommands(guild.id)
}
}, 5000)5. Dice Roll Logging
Log dice rolls for statistics and history:
import { rollDice } from '@crit-fumble/core'
async function handleDiceCommand(notation: string, context: DiscordContext) {
const result = rollDice(notation)
// Log to Core for stats
await api.dice.log({
notation: result.notation,
rolls: result.rolls,
modifier: result.modifier,
total: result.total,
isCrit: result.isCrit,
isFumble: result.isFumble,
label: 'User Roll',
discordId: context.userId,
guildId: context.guildId,
channelId: context.channelId,
source: 'discord'
})
return result
}6. Campaign & Session Integration
Access campaign data for game sessions:
// Get active campaign for channel
const { campaigns } = await api.campaigns.list({
guildId: interaction.guildId,
status: 'active'
})
// Log session message
await api.messages.log(sessionId, {
discordId: interaction.user.id,
content: message.content,
messageType: 'ic', // in-character
metadata: { channelId: interaction.channelId }
})Web Application (Activities on Web)
Serve Discord Activities both embedded in Discord and as standalone web apps.
1. Activity Server Setup (Vite/React)
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: process.env.CORE_API_URL || 'https://core.crit-fumble.com',
changeOrigin: true,
headers: {
'X-Core-Secret': process.env.CORE_SECRET || ''
}
},
'/.proxy': {
target: process.env.CORE_API_URL || 'https://core.crit-fumble.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/.proxy/, ''),
headers: {
'X-Core-Secret': process.env.CORE_SECRET || ''
}
}
}
}
})2. Discord Embedded App SDK Integration
// src/discord.ts
import { DiscordSDK, DiscordSDKMock } from '@discord/embedded-app-sdk'
export const isEmbedded = window.location.search.includes('frame_id')
export const discordSdk = isEmbedded
? new DiscordSDK(import.meta.env.VITE_DISCORD_CLIENT_ID)
: new DiscordSDKMock(import.meta.env.VITE_DISCORD_CLIENT_ID, null, null)
export async function initializeDiscord() {
await discordSdk.ready()
// Authorize with Discord
const { code } = await discordSdk.commands.authorize({
client_id: import.meta.env.VITE_DISCORD_CLIENT_ID,
response_type: 'code',
state: '',
prompt: 'none',
scope: ['identify', 'guilds']
})
// Exchange code with Core for session
const response = await fetch('/.proxy/api/activity/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
})
const { access_token } = await response.json()
// Authenticate with Discord SDK
await discordSdk.commands.authenticate({ access_token })
return {
guildId: discordSdk.guildId,
channelId: discordSdk.channelId,
instanceId: discordSdk.instanceId
}
}3. Standalone Web Mode (No Discord)
// src/App.tsx
import { CoreApiClient } from '@crit-fumble/core'
import { discordSdk, isEmbedded, initializeDiscord } from './discord'
function App() {
const [context, setContext] = useState<DiscordContext | null>(null)
const [api, setApi] = useState<CoreApiClient | null>(null)
useEffect(() => {
async function init() {
if (isEmbedded) {
// Discord Activity mode
const ctx = await initializeDiscord()
setContext(ctx)
setApi(new CoreApiClient({
baseUrl: '/.proxy',
getDiscordContext: () => ctx
}))
} else {
// Standalone web mode - use session auth
const client = new CoreApiClient({
baseUrl: '/api',
credentials: 'include'
})
// Check if logged in
const { user } = await client.auth.me()
if (!user) {
window.location.href = '/api/auth/signin/discord'
return
}
setApi(client)
}
}
init()
}, [])
if (!api) return <div>Loading...</div>
return <ActivityUI api={api} context={context} />
}4. Voice Controls from Activity
Request FumbleBot to join voice from an Activity:
import type { VoiceStatusResponse } from '@crit-fumble/core'
function VoicePanel({ api, guildId }: { api: CoreApiClient, guildId: string }) {
const [status, setStatus] = useState<VoiceStatusResponse | null>(null)
// Poll voice status
useEffect(() => {
const poll = async () => {
const status = await api.voice.getStatus(guildId)
setStatus(status)
}
poll()
const interval = setInterval(poll, 5000)
return () => clearInterval(interval)
}, [api, guildId])
const handleJoin = async (channelId: string) => {
await api.voice.join({ guildId, channelId })
// FumbleBot will receive command via WebSocket and join
}
const handleLeave = async () => {
await api.voice.leave({ guildId })
}
const handlePlay = async (url: string) => {
await api.voice.play({ guildId, url, volume: 0.5 })
}
return (
<div>
{status?.connected ? (
<>
<p>Connected to: {status.channelName}</p>
<p>Participants: {status.participants.length}</p>
<button onClick={handleLeave}>Leave Voice</button>
<button onClick={() => handlePlay('/audio/fanfare.mp3')}>Play Sound</button>
</>
) : (
<ChannelSelector onSelect={handleJoin} />
)}
</div>
)
}Container Service (VTT Management)
Manage FoundryVTT and other game containers with full lifecycle control.
1. Service Setup
import { CoreApiClient } from '@crit-fumble/core'
const api = new CoreApiClient({
baseUrl: process.env.CORE_API_URL!,
serviceSecret: process.env.CORE_SECRET!
})2. Container Lifecycle
// Create a new Foundry container for a user
async function provisionFoundryServer(userId: string, guildId: string) {
// Get user's license
const { licenses } = await api.foundry.getLicenses()
const license = licenses.find(l => l.userId === userId)
if (!license) {
throw new Error('User has no Foundry license linked')
}
// Create container
const { container } = await api.foundry.createContainer({
guildId,
foundryVersion: '12',
worldName: 'New Campaign'
})
// Wait for container to be ready
let status = await api.container.get(container.containerId)
while (status.status !== 'running') {
await new Promise(r => setTimeout(r, 2000))
status = await api.container.get(container.containerId)
}
return container
}
// Stop container when session ends
async function shutdownContainer(containerId: string) {
// Create backup first
const { worlds } = await api.foundry.listWorlds(containerId)
for (const world of worlds) {
await api.foundry.backupWorld(containerId, world.id)
}
// Stop container
await api.container.stop(containerId)
}3. World & Module Management
// Install modules for a game system
async function setupDnD5eWorld(containerId: string) {
// Install core modules
const modules = [
'https://github.com/foundryvtt/dnd5e/releases/latest/download/module.json',
'https://github.com/trioderegion/midi-qol/releases/latest/download/module.json'
]
for (const manifest of modules) {
await api.foundry.installModule(containerId, manifest)
}
// Update settings
await api.foundry.updateSettings(containerId, {
'core.defaultToken': { displayName: 1 },
'dnd5e.currencyWeight': true
})
}
// Backup world before major changes
async function safeWorldUpdate(containerId: string, worldId: string, update: () => Promise<void>) {
// Create backup
await api.foundry.backupWorld(containerId, worldId)
try {
await update()
} catch (error) {
// Could restore from backup if needed
console.error('Update failed, backup available')
throw error
}
}4. Container Monitoring
// Monitor container health
async function monitorContainers() {
const { containers } = await api.container.list({ status: 'running' })
for (const container of containers) {
const stats = await api.container.stats(container.id)
// Alert if resources are high
if (stats.cpu.percent > 80) {
console.warn(`Container ${container.id} CPU high: ${stats.cpu.percent}%`)
}
if (stats.memory.percent > 90) {
console.warn(`Container ${container.id} memory high: ${stats.memory.percent}%`)
}
// Check for errors in logs
const { logs } = await api.container.logs(container.id, {
level: 'error',
limit: 10
})
if (logs.length > 0) {
console.warn(`Container ${container.id} has ${logs.length} recent errors`)
}
}
}
// Run every 5 minutes
setInterval(monitorContainers, 5 * 60 * 1000)5. File Operations (Backup/Restore)
// Export world data
async function exportWorldData(containerId: string, worldId: string) {
const worldJson = await api.container.readFile(
containerId,
`/data/Data/worlds/${worldId}/world.json`
)
return JSON.parse(worldJson)
}
// Import custom content
async function importCustomContent(containerId: string, content: object) {
await api.container.writeFile(
containerId,
'/data/Data/custom-content.json',
{ content: JSON.stringify(content, null, 2) }
)
}6. License Pool Management (Admin)
// Manage shared license pool
async function manageLicensePool() {
// Get pool stats
const stats = await api.foundry.getPoolStats()
console.log(`Pool: ${stats.available}/${stats.total} licenses available`)
// Add license to pool
await api.foundry.addPoolLicense({
licenseKey: 'FOUNDRY-LICENSE-KEY',
notes: 'Sponsored license for community servers'
})
// List all pool licenses
const { licenses } = await api.foundry.listPoolLicenses()
for (const license of licenses) {
console.log(`${license.id}: ${license.inUse ? 'In Use' : 'Available'}`)
}
}CFG-X Adventure Engine (Text-Based RPG)
The CFG-X Adventure Engine is a text-based RPG system with World Anvil integration for lore management.
Architecture
Container Service (Game Engine) Core (Data Layer + UI)
┌────────────────────────────────┐ ┌─────────────────────────────────┐
│ Adventure Terminal │ │ /api/cfgx/* │
│ - Text parser & commands │─────▶│ - Manifest (bootstrap) │
│ - Game loop & state │ POST │ - Characters, Locations, Items │
│ - Combat system │ │ - Events → World Anvil timeline│
│ - NPC dialogue │ │ │
└────────────────────────────────┘ │ Activity UI (Discord Embed) │
│ - Game interface │
│ - Character sheet │
└────────────┬────────────────────┘
│ Sync
▼
┌─────────────────────────────────┐
│ World Anvil │
│ - Lore source of truth │
│ - Character stats (prototype) │
│ - Timeline events │
└─────────────────────────────────┘1. Service Setup
import { CoreApiClient } from '@crit-fumble/core'
const api = new CoreApiClient({
baseUrl: process.env.CORE_API_URL!,
serviceSecret: process.env.CORE_SECRET!
})
// Universe ID for CFG-X
const CFG_X_UNIVERSE_ID = '3703992e-6090-4e5a-b4d8-5c37effdb9a8'2. Game Engine Bootstrap (Container Service)
// Get full manifest for game initialization
const manifest = await api.cfgx.getManifest(CFG_X_UNIVERSE_ID)
console.log(`Universe: ${manifest.universe.name}`)
console.log(`Locations: ${manifest.locations.length}`)
console.log(`NPCs: ${manifest.npcs.length}`)
console.log(`Creatures: ${manifest.creatures.length}`)
console.log(`Items: ${manifest.items.length}`)
console.log(`Starting at: ${manifest.startingLocationId}`)
// Initialize game state with manifest data
const gameState = {
locations: new Map(manifest.locations.map(l => [l.slug, l])),
npcs: new Map(manifest.npcs.map(n => [n.slug, n])),
creatures: new Map(manifest.creatures.map(c => [c.slug, c])),
items: new Map(manifest.items.map(i => [i.slug, i])),
}3. Character Management
import type {
CfgxCharacter,
CfgxCreateCharacterRequest,
CfgxUpdateCharacterRequest
} from '@crit-fumble/core'
// Create new player character
const { character } = await api.cfgx.createCharacter(CFG_X_UNIVERSE_ID, {
slug: 'player-gandalf',
name: 'Gandalf the Grey',
title: 'Wizard',
characterClass: 'mystic',
statMind: 9,
statBody: 5,
statSoul: 8
})
// Derived stats are calculated automatically:
// HP = body × 10 = 50
// SP = soul × 5 = 40
// Stamina = (body + mind) × 3 = 42
// Update character state during gameplay
await api.cfgx.updateCharacter(CFG_X_UNIVERSE_ID, character.id, {
currentHp: 35,
currentLocationId: 'tavern-entrance',
inventory: {
backpack: ['health-potion', 'iron-key'],
gold: 150
},
equipment: {
mainHand: 'staff-of-power',
armor: 'grey-robes'
},
flags: {
quest_innkeeper_helped: true,
discovered_secret_passage: false
}
})
// Get user's character
const { character: myChar } = await api.cfgx.getPlayerCharacter(
CFG_X_UNIVERSE_ID,
userId
)4. Location Navigation
// Get location with exits and contents
const { location } = await api.cfgx.getLocation(CFG_X_UNIVERSE_ID, 'tavern-entrance')
console.log(location.name) // "The Rusty Tankard"
console.log(location.description) // Full room description
console.log(location.exits) // { north: 'market-square', east: 'tavern-bar', down: 'cellar' }
console.log(location.npcIds) // ['innkeeper-bob', 'mysterious-stranger']
console.log(location.itemIds) // ['notice-board', 'dusty-chest']
// List all locations with filters
const { locations, total } = await api.cfgx.listLocations(CFG_X_UNIVERSE_ID, {
locationType: 'room',
limit: 50
})5. World Anvil Integration
CFG-X character stats are stored in World Anvil's characterPrototype field as JSON:
import type {
CfgxSetCharacterStatsRequest,
CfgxWorldAnvilArticle
} from '@crit-fumble/core'
// Set character stats directly in World Anvil
// Stats format: {"mind": 7, "body": 5, "soul": 8, "class": "mystic", "level": 3}
await api.cfgx.setCharacterStats(
CFG_X_UNIVERSE_ID,
'388f860b-a308-40db-a5d7-4d2201d4e2ff', // World Anvil article ID
{
mind: 7,
body: 5,
soul: 8,
class: 'mystic',
level: 3,
xp: 1500
}
)
// Get World Anvil article details
const { article } = await api.cfgx.getWorldAnvilArticle(
CFG_X_UNIVERSE_ID,
'388f860b-a308-40db-a5d7-4d2201d4e2ff'
)
console.log(article.title) // "Fumble"
console.log(article.characterPrototype) // '{"mind":7,"body":5,"soul":8,...}'
// List all World Anvil articles in universe
const { articles, count } = await api.cfgx.listWorldAnvilArticles(CFG_X_UNIVERSE_ID)
for (const a of articles) {
console.log(`${a.entityClass}: ${a.title} (${a.id})`)
}
// Output: person: Fumble (388f860b-...), location: Tavern (abc123-...)6. Content Sync (Admin)
// Pull content from World Anvil → Core database
const pullResult = await api.cfgx.syncPull(CFG_X_UNIVERSE_ID)
console.log(`Synced: ${pullResult.summary.totalSynced} items`)
console.log(`Errors: ${pullResult.summary.totalErrors}`)
console.log(pullResult.results)
// {
// locations: { synced: 15, errors: 0 },
// characters: { synced: 8, errors: 1 },
// creatures: { synced: 12, errors: 0 },
// items: { synced: 45, errors: 0 }
// }
// Push game events to World Anvil timeline
const pushResult = await api.cfgx.syncPush(CFG_X_UNIVERSE_ID)
console.log(`Pushed ${pushResult.pushed} events to timeline`)7. Game Events
import type { CfgxCreateEventRequest, CfgxEventType } from '@crit-fumble/core'
// Log significant game events (synced to World Anvil timeline)
await api.cfgx.createEvent(CFG_X_UNIVERSE_ID, {
eventType: 'boss_kill',
title: 'The Dragon Falls',
description: 'Gandalf defeated the ancient wyrm in single combat.',
characterId: character.id,
locationId: 'dragon-lair',
locationName: 'The Dragon\'s Lair',
isPublic: true,
isCanon: true,
metadata: {
bossName: 'Smaug the Terrible',
lootObtained: ['dragon-scale-armor', 'hoard-gold']
}
})
// Event types: quest_complete, boss_kill, discovery, death, level_up, achievement8. Stat Formulas
CFG-X uses a simple 3-stat system (1-10 scale):
| Stat | Description | Derived Values | |------|-------------|----------------| | Mind | Intelligence, perception, magic | - | | Body | Strength, endurance, physical | HP = body × 10 | | Soul | Charisma, willpower, spirit | SP = soul × 5 | | - | - | Stamina = (body + mind) × 3 |
Character classes: explorer, scholar, warrior, mystic
Discord Activity Auth Keep-Alive (Web)
When serving Discord Activities on the web (outside Discord client), sessions need to be kept alive to prevent logout during gameplay.
1. Auth Flow for Web Activities
// src/hooks/useActivityAuth.ts
import { useEffect, useRef, useState } from 'react'
import { CoreApiClient } from '@crit-fumble/core'
export function useActivityAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const keepAliveRef = useRef<NodeJS.Timeout>()
useEffect(() => {
const api = new CoreApiClient({
baseUrl: '/api',
credentials: 'include'
})
async function initAuth() {
try {
// Check if already logged in
const { user } = await api.auth.me()
if (user) {
setUser(user)
startKeepAlive(api)
} else {
// Redirect to Discord OAuth
window.location.href = '/api/auth/signin/discord?callbackUrl=' +
encodeURIComponent(window.location.pathname)
}
} catch (error) {
console.error('Auth check failed:', error)
} finally {
setLoading(false)
}
}
function startKeepAlive(api: CoreApiClient) {
// Ping session every 5 minutes to prevent expiry
keepAliveRef.current = setInterval(async () => {
try {
await api.auth.session()
} catch (error) {
console.error('Session keep-alive failed:', error)
// Session expired, trigger re-auth
setUser(null)
}
}, 5 * 60 * 1000) // 5 minutes
}
initAuth()
return () => {
if (keepAliveRef.current) {
clearInterval(keepAliveRef.current)
}
}
}, [])
return { user, loading }
}2. Activity Route Handler (Next.js)
// app/activity/[...path]/page.tsx
import { fetchCoreSession } from '@crit-fumble/core'
import { redirect } from 'next/navigation'
import { cookies } from 'next/headers'
export default async function ActivityPage({ params }: { params: { path: string[] } }) {
const cookieStore = cookies()
const session = await fetchCoreSession(process.env.CORE_API_URL!, {
secret: process.env.CORE_SECRET!,
cookie: cookieStore.toString()
})
if (!session.user) {
redirect('/api/auth/signin/discord?callbackUrl=/activity/' + params.path.join('/'))
}
return <ActivityClient userId={session.user.id} />
}3. Session Touch Endpoint
Core automatically extends sessions on access. For long-running activities, periodically call any authenticated endpoint:
// Lightweight session touch - just checks session validity
const response = await fetch('/api/auth/session', {
credentials: 'include'
})
if (!response.ok) {
// Session expired, need to re-authenticate
window.location.href = '/api/auth/signin/discord'
}4. Cross-Origin Activity Setup
For activities served from a different domain:
// vite.config.ts (Activity dev server)
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://core.crit-fumble.com',
changeOrigin: true,
cookieDomainRewrite: 'localhost',
headers: {
'X-Core-Secret': process.env.CORE_SECRET || ''
}
}
}
}
})Production activities should be served from *.crit-fumble.com to share the auth cookie domain.
Environment Variables Summary
# All Services
CORE_API_URL=https://core.crit-fumble.com
CORE_SECRET=your-service-secret
# FumbleBot Additional
CORE_WS_URL=wss://core.crit-fumble.com
DISCORD_TOKEN=your-discord-bot-token
# Web App Additional
VITE_DISCORD_CLIENT_ID=your-discord-client-id
AUTH_SECRET=random-auth-secret
# Container Service Additional
DOCKER_HOST=unix:///var/run/docker.sockResponse Signing (Service-to-Service Security)
Core signs certain responses with HMAC-SHA256 for authenticity verification. This prevents man-in-the-middle attacks and ensures response integrity.
How It Works
- Request: Service sends
X-Core-Secretheader to prove identity - Response: Core signs the response body and includes
X-Response-Signatureheader - Verify: Service verifies signature matches using the same secret
Quick Start with SDK
import { fetchCoreSession, signedFetch, verifyResponseSignature } from '@crit-fumble/core'
// Option 1: Use the convenience function for sessions
const session = await fetchCoreSession(process.env.CORE_API_URL!, {
secret: process.env.CORE_SECRET!,
cookie: request.headers.get('cookie') || undefined
})
if (session.user) {
console.log(`Authenticated as ${session.user.name}`)
}
// Option 2: Use signedFetch for any endpoint
const { data, verified } = await signedFetch<{ campaigns: Campaign[] }>(
`${process.env.CORE_API_URL}/api/campaigns`,
{
secret: process.env.CORE_SECRET!,
method: 'GET'
}
)
console.log(`Response verified: ${verified}`)Manual Verification
import { verifyResponseSignature } from '@crit-fumble/core'
// Make request with X-Core-Secret header
const response = await fetch(`${CORE_API_URL}/api/auth/session`, {
headers: {
'X-Core-Secret': process.env.CORE_SECRET!,
'Cookie': request.headers.get('cookie') || ''
}
})
// Get signature and body
const signature = response.headers.get('X-Response-Signature')
const body = await response.text()
// Verify signature
const isValid = verifyResponseSignature(body, signature, process.env.CORE_SECRET!)
if (!isValid) {
throw new Error('Response signature verification failed - possible tampering')
}
// Safe to parse
const data = JSON.parse(body)Next.js Middleware Example
// middleware.ts
import { fetchCoreSession } from '@crit-fumble/core'
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
// Verify session with Core (signature verified automatically)
const session = await fetchCoreSession(process.env.CORE_API_URL!, {
secret: process.env.CORE_SECRET!,
cookie: request.headers.get('cookie') || undefined
})
// Redirect to login if not authenticated
if (!session.user) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add user info to request headers for downstream use
const response = NextResponse.next()
response.headers.set('x-user-id', session.user.id)
response.headers.set('x-user-admin', String(session.user.isAdmin))
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}Express/Node.js Middleware Example
// middleware/auth.ts
import { fetchCoreSession } from '@crit-fumble/core'
import type { Request, Response, NextFunction } from 'express'
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
try {
const session = await fetchCoreSession(process.env.CORE_API_URL!, {
secret: process.env.CORE_SECRET!,
cookie: req.headers.cookie
})
if (!session.user) {
return res.status(401).json({ error: 'Not authenticated' })
}
// Attach user to request
req.user = session.user
next()
} catch (error) {
console.error('Auth verification failed:', error)
return res.status(500).json({ error: 'Authentication error' })
}
}
// Usage
app.get('/api/protected', requireAuth, (req, res) => {
res.json({ message: `Hello ${req.user.name}` })
})Creating Signatures (For Core API Development)
import { createResponseSignature } from '@crit-fumble/core'
// Sign a response body
const body = JSON.stringify({ user: userData, expires: session.expires })
const signature = createResponseSignature(body, process.env.CORE_SECRET!)
// Set header
response.setHeader('X-Response-Signature', signature)
response.send(body)Security Notes
- Timing-safe comparison: The SDK uses
crypto.timingSafeEqualto prevent timing attacks - HMAC-SHA256: Industry-standard algorithm for message authentication
- Shared secret: Both services must have the same
CORE_SECRETvalue - Signature scope: Currently applied to
/api/auth/sessionendpoint - Optional verification: If no signature header is present, the request still works but
verifiedwill befalse
Type-Safe API
All responses are fully typed:
import type {
Campaign,
Character,
User,
DiceRollResult,
KBArticle
} from '@crit-fumble/core'
// Type-safe campaign data
const campaign: Campaign = await api.campaigns.get(id)
campaign.name // string
campaign.status // 'active' | 'completed' | 'on_hold'
campaign.system // 'dnd5e' | 'pf2e' | 'cypher' | ...
// Type-safe dice rolls
const roll: DiceRollResult = rollDice('2d20kh1')
roll.notation // string
roll.rolls // number[]
roll.total // number
roll.isCrit // booleanError Handling
import { HttpError } from '@crit-fumble/core'
try {
const { campaign } = await api.campaigns.get('invalid-id')
} catch (error) {
if (error instanceof HttpError) {
console.error(`API Error ${error.status}:`, error.message)
console.error('Response:', error.response)
}
}Rate Limiting
The API includes rate limiting with headers:
const response = await api.campaigns.list()
console.log(response.headers['x-ratelimit-limit']) // "100"
console.log(response.headers['x-ratelimit-remaining']) // "95"
console.log(response.headers['x-ratelimit-reset']) // Unix timestampAdvanced Usage
Custom HTTP Client
import { HttpClient } from '@crit-fumble/core/client'
const http = new HttpClient({
baseUrl: 'https://core.crit-fumble.com',
headers: {
'X-API-Key': process.env.CORE_API_KEY
},
timeout: 10000
})
const { data } = await http.get('/api/campaigns', {
params: { guildId: 'guild_123' }
})Type Imports Only
For type-only usage (no runtime code):
import type { Campaign, User, DiceRollResult } from '@crit-fumble/core/types'
function processCampaign(campaign: Campaign): void {
console.log(campaign.name)
}Environment Variables
# Required
CORE_API_URL=https://core.crit-fumble.com
# For service authentication
CORE_API_KEY=your-api-key-here
# For internal services
CORE_SECRET=your-service-secret-here
# For Discord Activities
DISCORD_CLIENT_ID=your-discord-client-idBrowser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Node.js 18+
Related Packages
| Package | Description |
|---------|-------------|
| @crit-fumble/core | Main SDK (this package) |
| @crit-fumble/react | React components library |
| @crit-fumble/fumblebot-proxy | Express proxy middleware |
Changelog
v10.31.0 (2025-12-15)
- CFG-X Adventure Engine - Text-based RPG API with World Anvil integration
cfgx.getManifest()- Bootstrap game engine with full universe datacfgx.listLocations()/getLocation()- Navigate game worldcfgx.createCharacter()/updateCharacter()- Player character managementcfgx.listCreatures()/listItems()- Game content accesscfgx.createEvent()- Log game events for World Anvil timelinecfgx.syncPull()/syncPush()- Bi-directional World Anvil sync
- World Anvil Direct Integration - Programmatic lore management
cfgx.setCharacterStats()- Set CFG-X stats on WA Person articlescfgx.getWorldAnvilArticle()- Retrieve article detailscfgx.listWorldAnvilArticles()- List all articles in universe- Stats stored in
characterPrototypefield as JSON
- Activity Auth Keep-Alive - Web activity session management documentation
- Session keep-alive hook for React apps
- Next.js route handler examples
- Cross-origin activity setup guide
v10.20.0 (2025-12-12)
- 🔐 Response Signing - HMAC-SHA256 signature verification for service-to-service security
verifyResponseSignature(body, signature, secret)- Verify response authenticitycreateResponseSignature(body, secret)- Create HMAC-SHA256 signaturesignedFetch(url, options)- Fetch with automatic signature verificationfetchCoreSession(baseUrl, options)- Convenience function for session endpoint
- 🛡️ Security Features:
- Timing-safe comparison to prevent timing attacks
X-Response-Signatureheader on/api/auth/sessionresponsesX-Core-Secretheader verification for service identity
- 📖 Implementation Guides:
- Next.js middleware example
- Express/Node.js middleware example
- Manual verification example
v10.18.0 (2025-12-12)
- 🎤 Voice Session Tracking - Full voice state management
voice.updateSession()- Report voice state from FumbleBotvoice.endSession()- End voice session when leavingvoice.getStatus()- Get voice status for a guildvoice.getSessions()- List all active voice sessions
- 🔊 Voice Commands - Request voice actions from Activities/Web
voice.join()- Request FumbleBot join a voice channelvoice.leave()- Request FumbleBot leave voicevoice.play()- Request audio playbackvoice.stop()- Stop audio playbackvoice.listenStart()/voice.listenStop()- Wake word listening
- 🌐 Voice WebSocket - Real-time command delivery
wss://core.crit-fumble.com/api/voice/ws- WebSocket endpoint- Subscribe to guilds, receive commands in real-time
- Acknowledge command completion/failure
- Types:
VoiceServerMessage,VoiceClientMessage,VoiceCommand
- 📝 Voice Transcripts - Log voice-to-text transcriptions
voice.logTranscript()- Log a transcriptionvoice.getTranscripts()- Retrieve transcripts
- 📖 Implementation Guides - Comprehensive setup documentation
- FumbleBot: WebSocket integration, voice reporting, command handling
- Web: Discord Activity + standalone web mode
- Container Service: VTT lifecycle, monitoring, file operations
v10.15.0 (2025-12-09)
- 🔐 Multi-Provider OAuth - Support for multiple OAuth providers
- Added Twitch OAuth provider (Discord, GitHub, Twitch now supported)
auth.providers()- Get list of available OAuth providersauth.linkedAccounts()- Get user's linked OAuth accounts- Automatic account linking when users sign in with matching emails
- 📖 New endpoints for web client auth integration:
GET /api/auth/providers- List available OAuth providersGET /api/auth/me/accounts- List user's linked OAuth accounts
v10.16.0 (2025-12-09)
- 🔐 Cross-Subdomain Auth - Session sharing across *.crit-fumble.com
- Cookies now set with
domain=.crit-fumble.comin production - www.crit-fumble.com can validate sessions from core.crit-fumble.com
- Cookies now set with
- ✨ New Session API for web apps:
auth.getSigninUrl(provider, callbackUrl)- Get OAuth signin URLauth.session()- Get current session (returns user with discordId, isAdmin)auth.signout()- Destroy session and clear cookie
- 📖 New endpoints:
GET /api/auth/session- Get session with full user infoPOST /api/auth/signout- Sign out and clear cookie
v10.14.2 (2025-12-09)
- 🔒 Security: Added HSTS header (Strict-Transport-Security) for production
- Enforces HTTPS for 1 year with includeSubDomains
- Protects against protocol downgrade attacks
v10.14.0 (2025-12-09)
- ✨ Container API - Full container orchestration
stats()- Get CPU, memory, network statisticslogs()/dockerLogs()- Application and Docker logsexec()- Execute commands in containersreadFile()/writeFile()/deleteFile()- File operationslistTemplates()/createFromTemplate()- Template managementlistShared()/getSharedAdventureTerminal()- Shared containers
- ✨ Foundry VTT Management - Full VTT control
listWorlds()/getWorld()/backupWorld()- World managementlistModules()/installModule()/uninstallModule()- Module managementlistSystems()- Game system listinggetSettings()/updateSettings()- Settings managementsetAdminPassword()/listUsers()- Admin operations
- 📖 Added Web Application Auth Proxy setup documentation
v10.4.0 (2025-12-02)
- ✨ Added dice rolling utilities with
@dice-roller/rpg-dice-roller - 📚
rollDice()- Roll dice with advanced notation support - ✅
validateDiceNotation()- Validate notation without rolling - 📖 Updated KB with corrected dice notation syntax
v10.3.0
- ✨ Added FoundryVTT container management API
- 🔧 Improved TypeScript types for campaigns
v10.2.0
- ✨ Added Knowledge Base API (362 articles)
- ✨ Added Campaign Management API
- ✨ Added Players, Sessions, and Messages APIs
v10.1.0
- ✨ Auth-agnostic dice roll logging
- 🔧 Support for both
userIdanddiscordId
Support
License
MIT © Crit-Fumble
