WorldForge Studio

00 — Quickstart

Install the WorldForge SDK, mint an API key, and read a world in three steps.

The SDK (@blueforge-studio/worldforge-sdk) is TypeScript-first but plain JavaScript everywhere: it is an ESM package built on fetch, with no framework, React or DOM requirement. Four entrypoints cover REST (.), offline world tools (/offline), realtime (/realtime: SSE + realm WebSocket) and MCP (/mcp). The package is ESM-only (exports has import conditions, no require).

Install

The package is published to the BlueForge registry (packr):

# ~/.npmrc (or the project .npmrc)
@blueforge-studio:registry=https://api.packr.blueforge.studio

pnpm add @blueforge-studio/worldforge-sdk
# or: npm install @blueforge-studio/worldforge-sdk / yarn add …

Anonymous reads are allowed — no auth token is needed just to install.

Mint an API key

API keys are created through the API with a user session (a browser session JWT — an API key cannot mint or revoke keys, by design). The SDK has no wrapper for /api-keys; call it with curl or client.raw:

curl -X POST https://api.worldforge.blueforge.studio/api-keys \
  -H "authorization: Bearer $USER_JWT" \
  -H 'content-type: application/json' \
  -d '{"name": "my-integration", "scopes": ["world:read", "world:write"]}'
# → 201 { "id": …, "key": "wk_live_….…", "expiresAt": "…" }

Keys are wk_live_<id>.<secret>, hashed at rest, scoped (see 01 — Authentication) and expire (90 days by default, 365 max; pass an ISO expires_at in the body to choose). Scopes must come from the catalogue — * is refused with 400 invalid_scopes. Store the secret immediately — only the hash is kept, and the response does not echo the scopes back.

First request

import { createClient } from '@blueforge-studio/worldforge-sdk';

const client = createClient({
  baseUrl: 'https://api.worldforge.blueforge.studio',
  apiKey: process.env.WORLDFORGE_API_KEY!,
});

const worlds = await client.worlds.list();                 // WorldSummary[]
const world = await client.worlds.get(worlds[0]!.id);      // WorldDetail (has `json`)

// The canonical world document — migrate, then validate, offline.
// Migrate FIRST: the schema pins `world.schemaVersion` to the current
// version, so an older document fails `safeParseWorld` until it is migrated.
import { safeParseWorld, runMigrations, type World } from '@blueforge-studio/worldforge-sdk/offline';

const current = runMigrations(world.json as World); // old schema → WORLD_SCHEMA_VERSION
const parsed = safeParseWorld(current);
if (!parsed.ok) throw new Error(parsed.errors.map((e) => `${e.path}: ${e.message}`).join('; '));
const doc = parsed.data;

Errors

Non-2xx responses throw WorldForgeApiError, which preserves the API's { error, reason } envelope:

import { WorldForgeApiError } from '@blueforge-studio/worldforge-sdk';

try {
  await client.worlds.patch(id, { confirm: true });
} catch (err) {
  if (err instanceof WorldForgeApiError) {
    console.error(err.status, err.code, err.reason, err.requestId);
    // e.g. 403 'forbidden' + reason, or 400 'commit_required'
  }
}

WorldForgeApiError carries status, body (the parsed JSON, or the raw text / null when the body was not JSON), code (body.error), reason (body.reason) and requestId (the x-request-id response header, when the server sent one). A misconfigured client (no auth option) throws WorldForgeConfigError at createClient time instead.

Next