Accounts, members and sharing
These calls are part of @onlyworlds/sdk. They work with a person’s OnlyWorlds account rather than one world: signing in, inviting people and AIs into a world, making a world, and changes a member proposes instead of writing.
Most of them need an account token (ow_a_), which stands for the person’s account on one device (see Keys and PINs). In the examples, account is a client made with one: new OwV2Client({ apiKey: token }). Installing the package, reading and writing are on the TypeScript SDK page.
Sign in with a password
Section titled “Sign in with a password”signInWithPassword turns an OnlyWorlds email (or username) and password into an account token (ow_a_) for this device. A client with no key can call it. With OW_EMAIL and OW_PASSWORD in a .env file, run this with node --env-file=.env signin.mjs:
import { OwV2Client } from '@onlyworlds/sdk';
const { token } = await new OwV2Client().signInWithPassword({
email: process.env.OW_EMAIL, // or username: '...'
password: process.env.OW_PASSWORD,
tokenName: 'My app on iPhone', // the name the token is listed under
});
const account = new OwV2Client({ apiKey: token }); // store the token: it is shown oncePass exactly one of email and username; both or neither is a TypeError before anything is sent.
The SDK signs in at onlyworlds.com’s sign-in service, mints the token with POST /api/v2/account/tokens, then ends the sign-in session, whether the mint worked or not. Only the token is kept: the password is sent once and held nowhere.
The answer is { token, id, name, last4 }. The token is listed under tokenName on the account’s tokens page (Settings → Account tokens in the account portal), where it can be revoked.
A refusal throws OwSignInError, whose reason is one of:
invalid_credentials: a wrong email, username or passwordemail_unverified: the account’s email address is not confirmed yetrate_limited: too many attempts; waiterr.retryAfterseconds when it is setoffline: onlyworlds.com was not reachedmint_failed: signed in, but no token was madefailed: anything else, with the server’s message
The password is never in an error.
A tool with a web page can instead send the person to onlyworlds.com to sign in, so the tool never sees a password: Sign in with OnlyWorlds has the SDK’s signInWithOnlyWorlds and completeSignIn.
Invite links
Section titled “Invite links”A world’s owner can invite a person with a link (see Invite links). The code is the part after #:
const invite = await client.previewJoin(code); // who invites, which world, what role; sends no key
const membership = await account.acceptJoin(code); // account: a client with an ow_a_ tokenA bad code is one 404 (err.isNotFound). Accepting can answer err.isAlreadyMember; it needs no account PIN, though a write key for the new membership waits for one. A refusal never uses up the code.
The owner makes, lists and revokes the links with the account token. The code comes back once, in the answer that makes it:
const made = await account.createWorldLink(worldId, { reader: 'person', role: 'contributor', days: 7 });
// made.line: the one line to send (who invites, to which world, the link); made.code: the ow_j_ code
const links = await account.listWorldLinks(worldId); // no codes: status pending | used | revoked | expired
await account.revokeWorldLink(worldId, made.id);reader: 'agent' makes a link an AI agent redeems for a seat instead, with redeemJoin: the answer has kind: 'agent', the seat’s key and secret, and its .env lines. Anyone but the owner gets a 404.
reader: 'player' makes a link for a friend with no account, who reads the world as a guest playing one Character and writes nothing (see Members and sharing). The friend’s side needs no key at all:
const link = await account.createWorldLink(worldId, { reader: 'player', character: 'new' }); // or a Character's id
// on the friend's device:
const phone = new OwV2Client(); // no key: only health, previewJoin, redeemJoin and signInWithPassword work
const joined = await phone.redeemJoin(code, { name: 'Ana' }); // joined.kind === 'player'
const reader = new OwV2Client({ apiKey: joined.key }); // a read key, no PIN, shown onceThe code is spent on redeem, and the answer can’t be replayed: if it is lost, the owner sees the row never used, and sends a new link.
One player link can also seat a whole table (see A table of players). Each player picks one Character from the owner’s list, and a refusal spends nothing:
const table = await account.createWorldLink(worldId, { reader: 'player', maxUses: 4, characters: [aliceId, borinId] });
// on each player's device:
const seen = await phone.previewJoin(code); // seen.uses_left, seen.characters: [{ id, name, image_url, taken }]
try {
const joined = seen.name_required
? await phone.redeemJoin(code, { name: 'Ana' }) // no list: a fresh Character named for the player
: await phone.redeemJoin(code, { character: seen.characters.find((c) => !c.taken).id });
} catch (err) {
if (err.isCharacterTaken || err.isCharacterInvalid) { /* preview again and pick another */ }
else if (err.isWorldFull) { /* the world has its 50 players */ }
else throw err;
}Who is in a world
Section titled “Who is in a world”Once in, a world key can ask who it is and who else is there (see Who am I):
const me = await client.me(); // { world, scope, kind, role, membership, character }
const roster = await client.members(); // the roster's rows: { id, kind, role, character, status }An account token reads the roster too. On a client made with the token and world (new OwV2Client({ apiKey: token, world: worldId })), members() sends GET /members?world=<id> and reads it as the token’s own access there: the owner as the owner, and a member as their own member key. An account with no access gets a 404. Without world it refuses before sending.
A seat for your AI
Section titled “A seat for your AI”To let your own AI work in a world you own or belong to, make it a seat: its own membership, key and secret, with no link to pass around (see Giving your AI access). With the account token:
const made = await account.createSeat(worldId, { name: 'Quill', role: 'contributor', character: 'new' });
// made.env: the .env lines for the AI (OW_API_KEY, OW_API_PIN, OW_WORLD, ...), shown once
const seats = await account.listSeats(worldId); // yours; every seat for the owner; no keys
const renewed = await account.rekeySeat(worldId, made.seat.id); // a new key and secret; the old stop
await account.removeSeat(worldId, made.seat.id);role is at most your own; character is new (a Character of its own), none, or mine (it plays the Character you play).
None of these sends an Idempotency-Key, so a key is never stored for a replay: if an answer is lost, listSeats shows the seat, and rekeySeat gives it a new key.
A world on the account
Section titled “A world on the account”An account token makes a world: createAccountWorld({ name, pin, keyName, source, producer }) answers the world and its one write key, shown once.
A fresh account has no PIN yet, so the first create answers 422 with err.param === 'pin' until you send one, which becomes the account’s PIN. An account that has one must leave pin out.
keyName names the key (1 to 80 characters). source and producer are the folder’s provenance lines, for a tool that makes the world from a folder, kept as the world’s origin.
const acct = new OwV2Client({ apiKey: 'ow_a_…' });
const made = await acct.createAccountWorld({ name: 'Hyperion', keyName: 'my tool', producer: { tool: 'my-tool', version: '1.0.0', input: 'bare' } });The token also reads a world with no key minted: give the client the world’s id as world, and list, listAll, get, getWorld and draught read it as the account’s own access there. The account calls keep working on the same client. Any other world call on such a client (a write, changes, me, files) is a TypeError before anything is sent.
With no key at all, the same option is the public reader, and getWorld() then carries counts.
const reader = new OwV2Client({ apiKey: 'ow_a_…', world: worldId });
for await (const el of reader.listAll('character', { signal })) { /* … */ }
const open = new OwV2Client({ world: worldId }); // no key: what the public readsA tool that stores a world key on the device mints it with mintWorldKey(worldId, { keep: true }), so signing out keeps the key; without keep, revoking the token revokes the keys it minted.
Proposed changes
Section titled “Proposed changes”A member can propose a change for a keeper to accept instead of writing it, and a world’s owner can make every agent seat work that way.
const row = await client.propose('character', id, { fields: { reputation: 'Feared' }, evidence: 'Chapter 12', expires: new Date(Date.now() + 14 * 86_400_000).toISOString() });
const queue = await client.worldProposals({ state: 'open' }); // newest first; keepers see every row, others their own
const { proposal, element } = await client.acceptProposal('character', id, row.id); // the keeper's own write, one feed row
await client.declineProposal('character', id, row.id, { reason: 'not yet' });
await client.withdrawProposal('character', id, row.id); // the proposer's
for (const r of queue.data) if (r.kind === 'new_element') await client.reviewNewElement(r.element_type, r.element_id, { decision: 'accept' }); // a seat's new element: accept clears the mark, decline deletes itThe queue holds two kinds of row, OwProposal (kind: 'proposal', its fields only what differs and omitted what an upsert left empty) and OwProposedNewElement (kind: 'new_element'): narrow on kind before reading a proposal’s own keys. next_cursor is opaque.
reviewNewElement settles a new element a seat made under the switch; an element with no mark throws with err.isNotProposed.
With AI seats propose, a person accepts on for the world (agent_writes_proposed on getWorld(), read-only; patchWorld strips it), an agent seat’s patch, upsert or editLinks on an existing element answers 202. The client then throws OwProposedError (err.isProposed, err.proposal) rather than returning a body that is not the element; a bulk slot with status: 202 stays in the result, typed.
An accept that finds a proposed field moved throws OwStaleProposalError (409, err.isStaleProposal, err.conflicts, err.current); the other codes are isOwnProposal, isProposedOnly, isTooManyOpen and isFieldNotVisible.
Nothing about a proposal reaches the change feed: poll the queue to learn of new ones.
How a reader shows an element still proposed, and leaves it out of what it hands on, is on the readers page: Proposed elements.
