--- name: tinyworld-integrations description: Use when changing Tiny World Builder API, webhook, SSE, MCP, plugin, or automation examples. --- # Tiny World Integrations The app has browser-local integration points plus a small Netlify account backend: - Account/profile/cloud-save functions live under `netlify/functions/`. `profile.mjs`, `builds.mjs`, `share.mjs`, and `assets.mjs` are routed to `/api/profile`, `/api/builds`, `/api/share`, and `/api/assets` via each function's exported `config.path`. - Auth helpers should resolve the trusted site/Identity base from the same deploy-origin chain used elsewhere, including `TINYWORLD_SITE_URL`, before Netlify deploy URL fallbacks. Do not derive Identity verification targets from request-controlled origins. - PartyKit durable flush buffers must clear every successfully-posted pending bucket in the `res.ok` branch (resources, tax payouts, GOLD events, etc.) so retries do not duplicate already-granted durable rewards. - Worlds MMO grid size: the saved world payload (`data.gridSize`) is authoritative. Treat `worlds.grid_size` as cached metadata that can be stale; DTOs, previews, pricing/count derivation, and room entry should prefer/sync the payload size so an 8x8 map is not shown as 20x20 in multiplayer. - Wallet/social functions also live under `netlify/functions/`: `wallet.mjs` verifies Phantom-signed Solana wallet challenges and reads `$TINYWORLD` balances/activity from RPC, `wallet-payments.mjs` creates Solana Pay payment intents, `players.mjs` tracks online presence/search/chat requests/parties, and `livekit-token.mjs` issues LiveKit room tokens when `LIVEKIT_URL`, `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET` are configured. - Community functions: `community.mjs` (`/api/community`) backs the `/community` Discord-lite page — rooms, DMs, members, bans, blocks, invites; tables auto-create + seed on first request. Channel names are forced lowercase and the super-owner (`TINYWORLD_COMMUNITY_OWNER`, default `jasonkneen`) is made an owner of every room each request via `ensureCommunityDefaults`. Only staff (super-owner / `TINYWORLD_COMMUNITY_STAFF`) or a room owner can create/delete channels and ban; the same `admin` flag gates every privileged action. Members must pass an anti-AI human check (`community_verifications`) AND have a mandatory **Twitter/X handle** on their profile (GitHub optional) before they can post/DM/join — `saveSocials` writes the bare handles to `profiles.twitter`/`profiles.github` (idempotent `ALTER TABLE ... ADD COLUMN` in `ensureTables`; migration `20260615020000_add_profile_socials.sql`). Bootstrap returns `me.profileComplete`; the page shows a forced "Complete your profile" modal until Twitter is set and renders both handles on profile cards. Members can fully **edit their profile** (display name, bio, avatar, handles) via `saveProfile` (alias `saveSocials`). Avatars are an **allowlisted preset set** under `assets/avatars/*.png` (keys in `AVATAR_KEYS`) — no user image uploads, so no NSFW image risk. All user-authored text (display name + bio) is run through `checkTextSafety`, a two-layer filter (hard substrings + whole-word, leet/spacing-normalized) that rejects sexual / nudity / abusive / hateful content. Tested in `tests/community-profile.test.mjs`. `community.html` signs users in **in-page** (no bounce to the builder): it loads `vendor/tinyworld-auth.js` via the import map for Netlify Identity email login/signup and calls `/api/wallet` for Phantom login, storing the session under the shared `tinyworld:auth:wallet-session.v1` key. - Community moderation webhook: `community-webhook.mjs` (`/api/community/webhook`) is a server-to-server endpoint for an agent (Hermes) to ban/unban/block/hide or restore/delete messages/purge spam/delete rooms. Auth is a shared secret (`TINYWORLD_COMMUNITY_WEBHOOK_SECRET`) via `x-tinyworld-signature: sha256=` (preferred) or `x-webhook-secret`. Shared primitives live in `lib/community-moderation.mjs`; `community.mjs` also emits outbound `message.created` events to `HERMES_COMMUNITY_WEBHOOK_URL` (signed, fire-and- forget) so the agent can observe and react. Full reference: `docs/community-webhook.md`. - User auth is Netlify Identity. The browser bridge is self-hosted through `vendor/tinyworld-auth.js` with an import map to vendored `@netlify/identity` / `gotrue-js`; do not reintroduce a remote identity widget script. - The builder should not show working-looking account UI on hosts that cannot serve Netlify Identity. Treat 404/405 `/.netlify/identity/*` failures from the browser `getSettings()` probe or login calls as "auth unavailable": hide sign-in/account commands, keep local/static building usable, and leave cloud save/share/collab actions gated off. For local account work, use Netlify dev at `http://localhost:8888/tiny-world-builder`. - Profile image fields stored through `/api/profile`, `/api/admin-users`, or community preset-avatar saves must be absolute `http(s)` URLs. Preset avatar paths under `assets/avatars/*.png` are normalized with the trusted site origin from `TINYWORLD_SITE_URL` / Netlify `URL` / deploy URL envs before validation and persistence; already-absolute URLs are left unchanged. - Account API fetches must send `Authorization: Bearer ` when possible and `credentials: 'same-origin'` so Netlify Functions can resolve the current Identity user. Wallet login uses the same bearer path with signed `tw-wallet-v1...` session tokens stored under `tinyworld:auth:*`. - For local account/function work, run `npx netlify dev` and use `http://localhost:8888/tiny-world-builder`; that port keeps the auth/account UI enabled while the plain static dev server remains anonymous. - Cloud worlds are stored as full TinyWorld JSON in Netlify Database `builds` rows. Existing rows update through `PUT /api/builds?id=` so named localStorage worlds can stay bound to one cloud row instead of creating duplicates. Public share links create immutable-ish rows in `world_shares` and load through same-origin `?share=` / `/api/share?id=`. - Multiplayer/shared building uses PartyKit separately from Netlify Functions. `partykit.json` points at `party/index.js`, local development runs with `npm run party:dev` on port `1999`, and browser rooms connect only when a URL includes `?party=`, `?room=`, or `?collab=`. Collaborate links should reuse a `/api/share` id as both the world snapshot id and the PartyKit room id: `/tiny-world-builder?share=&party=`. - Shared build/collab rooms are public-observer by default: second and later PartyKit connections are admitted as `viewer` seats, not held in a lobby. Host clients heartbeat public room metadata to `/api/collabs`; the home page feed and `/collabs` page list those rooms with observer links (`observe=1`). This public visibility must not grant edit authority; edits still require a host-assigned role plus server-side island/zone checks. - Closing a shared build is a two-layer operation: host clients send `room.close` to PartyKit so every connected peer receives `room.closed` and no replacement host is promoted, and they POST `{ action: 'close', roomId }` to `/api/collabs` so the public registry stores a short-lived tombstone in `collab_room_closures`. Heartbeats for tombstoned rooms must return `{ closed: true }` instead of recreating the listing. - Admin collab moderation lives on `/collabs`: authenticated world-admin sessions call `/api/collabs` with `{ action: 'hide', roomId }` to add a short-lived `collab_room_hides` tombstone that removes a room from public lists without disconnecting occupants, or `{ action: 'adminClose', roomId }` to use the close tombstone. When a host sees that close tombstone on its registry heartbeat, the client must send PartyKit `room.close` before closing its socket so connected peers get the same shutdown event as a manual host stop. - Shared build owners are tracked from the `/api/share` row. `/api/collabs` copies `world_shares.owner_auth_id/profile_id` into `collab_rooms`, exposes `GET /api/collabs?mine=1` for the builder world-menu "Shared rooms" section, and lets the owner/admin `hide` (make private), `unhide`, or `ownerClose`. `GET /api/collabs?roomId=&control=1` can return a signed `tinyworld-collab-control` token; the builder sends `control.claim` to PartyKit so the original sharer/admin can reclaim host controls when reopening their own room link instead of staying an observer. - Collaborative build zones are transient PartyKit room permission data, not saved world cells. Host clients send `zones.set`; the server sanitizes zones, stores editor `zoneIds`, and must gate every non-host `cell.set` against assigned active zones. Client outlines/labels and local edit checks are UX and desync prevention only; do not rely on them as the authority. - MMO economy/multiplayer extraction lives in `packages/tinyworld-mmo-core/`. It is a dependency-free ESM package for shared GOLD allowance, resource tax, ledger, join-command, and interest-snapshot contracts. Use it when wiring the TinyWorld economy guide into PartyKit or Netlify Functions instead of copying constants between runtime files. - Tinyverse published-world navigation no longer uses the `tinyverse-nexus` hub. `/api/worlds` should hide that slug from lists and direct loads, published world data should normalize to one center `stargate` with `dest: '__world-picker'`, and PartyKit `safeSpawn()` should prefer that gate so players arrive where the in-world picker exit is. - Tinyverse/lobby access is locked to the Jason account allowlist in `netlify/functions/lib/tinyverse-access.mjs`. Do not use `accountMeetsCriteria()` or a raw `profiles.lobby_access` flag as the authoritative gate; migrations should keep `lobby_access` default false and clear it for every non-allowlisted profile. - Tinyverse room join/refresh payloads use compact cells. Terrain-only cells may be `[x,z,terrain]`; object/resource cells are `[x,z,terrain,kind]`. Keep the renderer validator and `applyState()` tolerant of both tuple lengths. - Explicit resource-bearing custom assets use object-form cells with `economy: { resource, charges?, label? }`. Live resources are currently `fish`, `ore`, `plants`, and `meat`; normalize through `packages/tinyworld-mmo-core/normalizeWorldResourceSpec(...)` in PartyKit and Netlify code instead of inferring resources from visual materials or copying constants. Compact tuple cells remain the default for ordinary terrain/kind saves. - Tinyverse multiplayer rooms are runtime/play/moderation surfaces only. Do not add live island building controls, `adminSave`, build-role seats, or `world.refresh` board replacement inside PartyKit rooms. Island editing and version publication must live in the dedicated draft/version flow. - Local custom assets are account data too: `/api/assets` stores one `asset_libraries` row per profile containing custom voxel-build stamps and saved asset templates. Browser hooks in `saveCustomVoxelBuildStamps()` and `saveAssetTemplates()` queue a cloud sync after login. - Local Netlify Database failures are expected in some `netlify dev` sessions. Translate 503 `Netlify Database is not available...` responses into a friendly account/cloud status or `warn` toast, never a red production-style error toast, raw database message, or visible `Local DB offline` wording. - Wallet/player social functions rely on `netlify/database/migrations/20260602120000_wallet_players_social.sql`. If those tables are missing in local Netlify dev, classify Postgres `42P01` with `isMissingRelations(...)` and return a setup-oriented 503 instead of logging raw missing-relation errors as generic 500s. - Phantom wallet linking and wallet login must stay challenge/response based: the browser asks Phantom to sign the server-issued message and the function verifies the Ed25519 signature against the Solana public key before linking or minting a wallet session. Do not accept a posted wallet address as proof of ownership. Wallet login requires `TINYWORLD_WALLET_SESSION_SECRET` (or `TINYWORLD_AUTH_SECRET`) for HMAC-signed challenge/session tokens. `$TINYWORLD` mint/payment values come from env (`TINYWORLD_TOKEN_MINT`, `TINYWORLD_PAYMENT_WALLET`, optional `SOLANA_RPC_URL`) rather than client constants. - Database schema changes belong in `netlify/database/migrations/*.sql`. Deploy previews get their own database branch, so use a preview deploy for real Identity + DB verification; local `netlify dev` is useful for functions but is not a complete Identity social-login test. Browser-local integration points: - Outbound webhooks live in `tiny-world-builder.html` under `// -------- API / webhooks / SSE bridge --------`. - Optional browser-local probes must be opt-in so the static app stays console-clean: the Cluso in-page embed is LOCAL-DEV-ONLY, injected at runtime by `tools/dev-server.js` (assets in gitignored `cluso/`); it must never be referenced by committed/shipped HTML; model-stamp API endpoints load only with `?modelApi=1`, `?modelStampApi=1`, `window.__TWB_MODEL_STAMP_API_ENABLED__ = true`, or `localStorage['tinyworld:features:model-stamp-api']='1'`. - `fireWebhook(event, payload)` batches editor mutations and POSTs `{ source: 'tiny-world-builder', events }` to the configured Developer-panel webhook URL. - Inbound automation uses `EventSource` against the configured Developer-panel SSE URL. Each SSE `data:` payload must be one JSON command accepted by `applyRemoteCommand`. - Supported inbound ops include `place` / `set_cell`, `clear`, `reset`, plus runtime-only vehicle controls: `vehicle_spawn`, `vehicle_set_goal`, `vehicle_controls`, `vehicle_remove`, and `vehicle_clear`. - Runtime vehicles must not pass through each other. Keep traffic behavior in the runtime layer: collision radius + yield radius, brake when another vehicle is inside the envelope, and reroute around occupied road cells after a short blockage when an alternate road path exists. - Placed objects on paths are live traffic blockers. `isVehicleDrivableCell` should allow path cells only when the main `kind`/extras do not occupy the tile, while bridge cells remain drivable. Call `refreshVehiclesForWorldObstacleChange` from world edit paths so active auto vehicles reroute immediately when the user drops or removes an obstacle. Examples live under `plugins/examples/`: - `webhook-receiver.js` captures outbound webhook batches. - `sse-command-relay.js` exposes `/sse` for the browser and `/command` for external clients. - `send-command.js` is a small CLI for the relay. - `mcp-stdio-bridge.js` is a dependency-free MCP stdio server that calls the relay and reads the webhook log. - `vehicle-road-demo.js` is a dependency-free MCP client/demo runner that talks to `mcp-stdio-bridge.js`, paints a visible road/water/bridge network, spawns runtime vehicles, and retargets them in a loop so the browser remains watchably active. - The app also supports browser-native shareable vehicle demo URLs: - `?demo=vehicles&seed=tide-ridge-428` creates the small/default visible road demo. - `?demo=vehicles-large&seed=metro-culdesac-20&stats=1` creates the default 20×20 scale test with arterial/ring roads, bridge crossings, cul-de-sac endpoints, and 36 autonomous vehicles on long routes. - Large-demo params: `size=` / `mapSize=` / `grid=` / `gridSize=` accept the nearest valid demo grid size from `12` through `20` (`12`, `16`, `20`); `cars=` / `carCount=` / `vehicles=` / `vehicleCount=` accept `1..120` and are capped by available unique endpoints. Keep these demos visually self-identifying: show an active badge, hide overlays that cover the road network, and make vehicles obvious with beacons/markers. During local demo work, `tools/dev-server.js` should make bare `http://localhost:3000/` and no-query `http://localhost:3000/tiny-world-builder` redirect to the small seed so the user can simply open the port or remembered app URL and watch it. Use the large URL explicitly for scale/perf checks. When changing command shape, update the app bridge and these examples together.