@onlyworlds/sdk 4.6.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # For AI agents using @onlyworlds/sdk
2
2
 
3
- **Current as of**: SDK **4.x** · schema-dist **v0.30.1-dist.15** (canonical 00.30.01).
3
+ **Current as of**: SDK **4.x** · schema-dist **v0.30.2-dist.16** (canonical 00.30.02).
4
4
  This line is asserted by `codegen:check` in CI — if the pin moves and this file is not
5
5
  re-read against it, the check fails rather than letting this document rot quietly.
6
6
 
@@ -21,7 +21,7 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
21
21
  - Known, deterministic operations (CRUD, sync, bulk) → **this SDK**. Typed calls, typed
22
22
  responses, far cheaper than tool-schema reasoning.
23
23
  - Live exploration of a user's world from a chat/agent context → the **MCP server** at
24
- `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 11 tools).
24
+ `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 12 tools).
25
25
 
26
26
  **Wire facts that bite** (full details in README):
27
27
  - Never send a `"world"` field in payloads — world identity comes from the API key (422 otherwise).
package/CHANGELOG.md CHANGED
@@ -1,24 +1,57 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to `@onlyworlds/sdk`. Maintained from 3.1.0 onward (Kael, Assembly);
3
+ All notable changes to `@onlyworlds/sdk`. Maintained from 3.1.0 onward;
4
4
  earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
- ## [4.6.0] — 2026-10-06 (staged, not published)
8
+ ## [4.7.1] — 2026-10-08
9
+
10
+ ### Changed
11
+ - **README rewritten**: install, a first run against the public demo world (its output shown,
12
+ run as written), your own world with an agent seat for writes, then the reference. It no
13
+ longer calls a `world` field in a payload an error: the API ignores it.
14
+ - Comments, the RFC and pin-file prose no longer name people or internal places.
15
+ - **Schema repinned `v0.30.1-dist.15` → `v0.30.2-dist.16`** (canonical **00.30.02**).
16
+ `ONLYWORLDS_VERSION` is now `'00.30.02'`. Field descriptions move with the schema: Location's
17
+ `rivalries` and `partnerships`, Marker's `order`, and Pin's `element` pair. No field shape and
18
+ no `FIELD_SCHEMA` entry changed: canonical now requires only `name` on every type, which is
19
+ what `FIELD_SCHEMA` already said, so its hand-made exception for the old Pin and Marker
20
+ lists is gone. If the schema lists more than `name` again, codegen stops and asks for a
21
+ ruling instead of dropping the list. 31/31 file hashes recomputed from the tag.
22
+
23
+ ## [4.7.0] — 2026-10-08
24
+
25
+ ### Added
26
+ - **`OwApiError.isResyncRequired`**: 409 `resync_required` from `/changes`. Guests can follow the
27
+ feed since keel D79; when a guest's view or a key's role changes, its stored cursor is refused. Drop
28
+ it, walk from zero, and replace the local copy rather than merging.
29
+
30
+ ### Documented
31
+ - `uploadImage` accepts webp, png, jpeg and avif. 4.6.0's JSDoc and README also listed gif, which
32
+ the edge refuses (415 `unsupported_type`); checked against the Worker's type table.
33
+ - `changes()`: the old rewind rule compared a stored cursor with `head`, which an opaque cursor
34
+ cannot do; replaced with the reset rule (`isResyncRequired`).
35
+ - The doc comments of `OwClientConfig`, `create` and `uploadImage` are written for users: internal
36
+ ruling numbers and test-harness notes are gone, and each opens with what the option or method does.
37
+ The docs site's TypeScript reference is generated from these comments.
38
+
39
+ ## [4.6.0] — 2026-10-06
40
+
41
+ Published from `e198298` (tag `v4.6.0`); the tarball was checked against a build of that commit.
9
42
 
10
43
  Includes 4.5.1's documentation fixes: 4.5.1 was staged and never published.
11
44
 
12
45
  ### Added
13
- - **`uploadImage(image, { key?, ticket? })`**: upload one image (webp, png, jpeg, avif, gif;
14
- never SVG) and get its permanent public URL. Two requests: a ticket from keel
46
+ - **`uploadImage(image, { key?, ticket? })`**: upload one image (webp, png, jpeg, avif;
47
+ never SVG; 4.6.0's docs also said gif, which the edge refuses with 415) and get its permanent public URL. Two requests: a ticket from keel
15
48
  (`POST /media/ticket`), then the bytes straight to the edge at the ticket's `upload_url`
16
49
  (keel never sees them). Only the ticket goes to the edge, never the API key or PIN. An image
17
50
  larger than the ticket's `max_bytes` throws 413 `too_large` before anything is sent; the edge's
18
51
  refusals (`ticket_used`, `ticket_expired`, `bad_key`, `exists`, `unsupported_type`, ...) arrive
19
52
  as `OwApiError` with their code. Set the returned `url` as an element's `image_url` yourself.
20
53
  Checked live on 2026-10-06: one upload through keel's ticket to the edge (201), and the same
21
- ticket again refused with 401 `ticket_used` (Keel #59).
54
+ ticket again refused with 401 `ticket_used`.
22
55
  - **`createMediaTicket()`**: the first step alone, for callers that upload themselves (progress
23
56
  bars, say). Types `OwMediaTicket` and `OwUploadedImage`.
24
57
 
@@ -33,11 +66,11 @@ Docs only: no code path changes.
33
66
  ### Documented
34
67
  - **`isOwnerOnly`'s JSDoc in 4.5.0 was wrong about who sees `owner_only`.** It said owners and
35
68
  co-builders never see it. A member key, a co-builder's too, gets it on `PATCH /world`; only the
36
- owner's key is exempt (keel spec section 9; confirmed from keel's `patch_world` by Skeld,
69
+ owner's key is exempt (keel spec section 9; confirmed from keel's `patch_world`,
37
70
  2026-10-04).
38
71
  - **`world_gone` is reserved.** `OwAuthErrorCode` and `isAuthError` still carry it, but keel never
39
72
  emits it: a deleted world's keys are deleted with it, so the key answers 401 `invalid_credentials`
40
- (Skeld, 2026-10-04; keel spec section 2, T6b). Found by the wire gate reading the SDK's own
73
+ (2026-10-04; keel spec section 2). Found by the wire gate reading the SDK's own
41
74
  built file for the codes it keys on.
42
75
 
43
76
  ## [4.5.0] — 2026-10-04
@@ -48,7 +81,7 @@ three behaviours the server has always had that no client doc said. Additive; no
48
81
  ### Added
49
82
  - **`OwApiError.isOwnerOnly`**: 403 `owner_only`, a contributor attempting a world-level change
50
83
  (`patchWorld`, publishing a show page). Owners and co-builders never see it. keel's OpenAPI
51
- document does not name the code yet (Skeld has that to close); the wire returns it.
84
+ document does not name the code yet (the schema side has that to close); the wire returns it.
52
85
 
53
86
  ### Documented
54
87
  - **An atomic `/bulk` rollback still returns 201 slots** for the items that would have
@@ -77,10 +110,10 @@ contributor meets on someone else's. Additive; nothing is removed or narrows.
77
110
  ### Fixed
78
111
  - **`FIELD_SCHEMA` no longer marks `marker.map/zone/x/y/order` and `pin.map/element_type/element_id/x/y`
79
112
  as `required: true`.** The wire requires `name` and nothing else (rulings.yaml `nullable-by-default`,
80
- Captain 2026-07-28; keel's OpenAPI write schemas say `required: [name]`), but the schema files still
113
+ ruling 2026-07-28; keel's OpenAPI write schemas say `required: [name]`), but the schema files still
81
114
  list those fields and the generator copied the lists, so a form built from the table demanded
82
115
  coordinates the server never did. Found by comparing the SDK with keel's OpenAPI document (the
83
- new `verify-sdk-wire` gate in Assembly). **If you read `required` from this table, marker and pin
116
+ new `verify-sdk-wire` gate). **If you read `required` from this table, marker and pin
84
117
  fields now read as optional**, which is what the server accepts. The generator notes the lists it
85
118
  ignores on every run and says when canonical stops carrying them.
86
119
 
@@ -128,7 +161,7 @@ Ids, honest filter docs, and the schema repin. Nothing is removed; no call needs
128
161
  order elements by id. For creation order use `created_at`; `change_seq` is last-write
129
162
  order. This is a **default, not a
130
163
  requirement**: a v4 or v7 id a caller supplies is still accepted as-is, and
131
- every id already stored stays valid. Keel mints v7 server-side too (Captain's ruling,
164
+ every id already stored stays valid. Keel mints v7 server-side too (ruling,
132
165
  2026-09-28). Pinned by six tests: version and variant, the big-endian timestamp above
133
166
  bit 32, fractional and pre-1970 clocks floored consistently, creation order across 50
134
167
  consecutive milliseconds, 1,000 distinct ids inside one millisecond, and the no-`crypto`
@@ -138,7 +171,7 @@ Ids, honest filter docs, and the schema repin. Nothing is removed; no call needs
138
171
  edges, and keel's `uuid7()` agrees on timestamp, version and variant for a pinned clock.
139
172
  - **`elementColor()` on an unknown type now throws a `TypeError` that names the type.** It
140
173
  used to crash with `Cannot read properties of undefined (reading 'dark')`. Found while
141
- building the Forge's colour gate: atlas's own copy falls back to the `world` family for an
174
+ building a colour gate: atlas's own copy falls back to the `world` family for an
142
175
  unknown type, so swapping it for this export is not a pure re-export for that input, and the
143
176
  docstring now says so. Whether the package should fall back is left to its consumers.
144
177
  - The generated field-schema comment no longer calls `maximum:` an open question or counts
@@ -285,7 +318,7 @@ pinned consumers.
285
318
  ### Added (post-alpha.0, same night)
286
319
  - **`ELEMENT_ICONS` GENERATED** from keel's `icon:` wrapper key (keel `56c124a`).
287
320
  - **`ELEMENT_SECTIONS` DERIVED** from the canonical schema's own document structure
288
- (Skeld's ruling: sections are the standard tier's property groups; document order is
321
+ (ruling: sections are the standard tier's property groups; document order is
289
322
  display order). Divergence check vs the old hand table found and fixed three fossils:
290
323
  creature "Behaviour"→"Behavior", pin's triple-listed generic link → `element`,
291
324
  relation "Involves" listing a nonexistent `relations` field. Canonical is truth.
@@ -294,18 +327,18 @@ pinned consumers.
294
327
  covered by the codegen drift guard. AGENTS.md points agents at it first.
295
328
  - **`OwV2Client.request()` is public** (typed escape hatch; does not sanitize) and
296
329
  structurally satisfies `TokenTransport` — `new TokenResource(client)` just works.
297
- Token ruling (Skeld): keel keeps `/tokens/*` long-term; ported, not deleted.
330
+ Token ruling: keel keeps `/tokens/*` long-term; ported, not deleted.
298
331
  - README rewritten v2-native (v1 sections, branded types, and the encrypted-key
299
332
  walkthrough removed; bulk partial-failure and idempotency-key hygiene promoted).
300
333
 
301
- ### alpha.1 — consumer-pin fixes (Temper's atlas review, same night)
334
+ ### alpha.1 — consumer-pin fixes (atlas review, same night)
302
335
  - **`parseEnvelope` → `parseErrorEnvelope`** (it parses the platform *error* envelope;
303
336
  renamed before the 4.0 name freeze to avoid collision with the world-export envelope).
304
337
  - The never-whitelist LAW now lives as a comment on `READ_ONLY_FIELDS` itself.
305
338
  - Stale v1 example in the shipped d.ts fixed (TokenResource JSDoc).
306
339
 
307
340
  ### Release gates — ALL GREEN (2026-07-23 night)
308
- - **Gate 3 (wire-log v1-traffic query, Skeld)**: GREEN — no unknown v1-SDK consumer on
341
+ - **Gate 3 (wire-log v1-traffic query)**: GREEN — no unknown v1-SDK consumer on
309
342
  the wire; observed v1 traffic is pinned deployed builds (council proxy, legacy plugin
310
343
  walks) that an npm major cannot touch. The v1 API dialect stays served regardless.
311
344
  - **`/api/v2/tokens/` live on prod + staging** (keel `e181689`; three-base byte
@@ -313,7 +346,7 @@ pinned consumers.
313
346
  "older server", not "no tokens". Requirement noted in the migration guide.
314
347
  - **`ONLYWORLDS_VERSION` now GENERATED** from canonical's `VERSION` file (carried
315
348
  into keel `schema/` by the refresh script, keel `492168c`) — the last hand-synced
316
- constant is dead. Consumer soak: Temper pinned the alpha against atlas's full
349
+ constant is dead. Consumer soak: the atlas app pinned the alpha against its full
317
350
  static surface — strict-clean compile, correct runtime, package shape approved.
318
351
  - **No `npm deprecate` on 3.x** (landscape research; 3.x stays plainly supported).
319
352
  - 4.x wishlist: account client (list/mint/create — atlas first consumer), world-export
@@ -327,7 +360,7 @@ pinned consumers.
327
360
  `ELEMENT_FAMILIES` (type → family), `FAMILY_COLORS` (family → `{light, dark}` hex),
328
361
  `familyOf(type)`, `elementColor(type, mode)`, `FAMILY_ORDER`, type `ElementFamily`.
329
362
  Four semantic families (agents / world / abstract / temporal) ruled 2026-07-22 after
330
- CVD-validated measurement (Orrery `product/schema/element-palette-measurements.md`).
363
+ CVD-validated measurement.
331
364
  Colour carries the FAMILY; `ELEMENT_ICONS` carries the TYPE. Keyed on the v2 slug
332
365
  union. First proven live in atlas and council, whose local copies become re-exports.
333
366
  **`ELEMENT_FAMILIES` is GENERATED** from the `family:` key in keel's schema YAML
@@ -355,7 +388,7 @@ pinned consumers.
355
388
 
356
389
  ## [3.0.0] — 2026-07-18
357
390
 
358
- - v2-native client (`OwV2Client`) absorbed from Assembly's ow-v2-client v0.9.0 (Kael),
391
+ - v2-native client (`OwV2Client`) absorbed from an earlier v2 client (ow-v2-client v0.9.0),
359
392
  wire-corrected against live staging fixtures (S22). Generated element types from the
360
393
  canonical keel schema YAML (`src/v2/types.generated.ts`), validated against all 1,086
361
394
  live W11 elements. v1 surface unchanged and frozen.
package/README.md CHANGED
@@ -3,74 +3,79 @@
3
3
  [![npm version](https://badge.fury.io/js/@onlyworlds%2Fsdk.svg)](https://www.npmjs.com/package/@onlyworlds/sdk)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- The canonical typed client for the [OnlyWorlds](https://onlyworlds.github.io) v2 API, plus the
7
- canonical constants (element types, icons, colour families, field schema) — generated from the
8
- canonical OnlyWorlds schema, obtained through the public
9
- [schema distribution](https://github.com/OnlyWorlds/schema-dist) at a pinned, hash-verified
10
- tag. The generated files carry that tag and commit in their header, so what these types were
11
- built from is checkable rather than asserted.
6
+ OnlyWorlds is an open standard for worldbuilding data: 22 element types (characters, locations, events, laws and the rest) that any tool can read and write. This is a typed client for its API, with the schema's constants (element types, icons, colour families, field metadata) generated from the published [schema distribution](https://github.com/OnlyWorlds/schema-dist). It runs in Node 18+ and in the browser through a bundler (ESM only).
12
7
 
13
- **4.x is v2-native and ESM-only (Node 18+).** If you need the legacy v1 API dialect
14
- (`OnlyWorldsClient`) or CommonJS `require()`, stay on 3.x — it remains published and the v1 API
15
- remains served. See [docs/migrating-3-to-4.md](docs/migrating-3-to-4.md).
16
-
17
- ## Installation
8
+ ## Install
18
9
 
19
10
  ```bash
20
11
  npm install @onlyworlds/sdk
21
12
  ```
22
13
 
23
- ## Quick Start
14
+ ## First run
15
+
16
+ Moppetopia is a public demo world. Its read-only key, `0000000001`, needs no account:
24
17
 
25
18
  ```typescript
26
19
  import { OwV2Client } from '@onlyworlds/sdk';
27
20
 
28
- const client = new OwV2Client({ apiKey: 'ow_r_your_key' }); // read-only key: that's all you need
29
- const page = await client.list('character'); // { data, has_more, next_cursor }
21
+ const client = new OwV2Client({ apiKey: '0000000001' });
22
+ console.log((await client.getWorld()).name);
23
+ const { data } = await client.list('character', { limit: 3 });
24
+ for (const c of data) console.log(' ', c.name);
25
+ ```
26
+
27
+ ```
28
+ Moppetopia
29
+ Admiral Splashworth
30
+ Admiral Fluffington
31
+ Captain Snoot
30
32
  ```
31
33
 
32
- Key kinds: `ow_w_` write / `ow_r_` read-only (no PIN — the "give this to your players" key) /
33
- `ow_a_` account Bearer / 10-digit legacy. Writes on a PIN-protected world also pass `apiPin`.
34
- Demo keys `0000000000`–`0000000009` read real sample worlds.
34
+ ## Your own world
35
35
 
36
- ## The full round-trip
36
+ Create a world at [onlyworlds.com](https://www.onlyworlds.com); its keys are on the world's page. A new key is shown once, so copy it then.
37
+
38
+ - An `ow_r_` key reads, with no PIN. It is the key to give to players or a public site.
39
+ - An `ow_w_` key reads and writes. Writes also send a PIN as `apiPin`.
40
+ - Anyone who loads a web page can read the keys in its code. Ship only an `ow_r_` key to a browser, and keep write keys and seat secrets on a server.
41
+ - For code that writes, give it its own [agent seat](https://onlyworlds.github.io/docs/development/agents), a key for one tool or script. The seat's key and its `ow_s_` secret (sent as `apiPin`) work in one world, and you can remove them without touching your account PIN.
37
42
 
38
43
  ```typescript
39
- const writer = new OwV2Client({ apiKey: 'ow_w_your_key', apiPin: '1234' });
44
+ const writer = new OwV2Client({ apiKey: 'ow_w_…', apiPin: 'ow_s_…' });
45
+ ```
46
+
47
+ ## Reading and writing
40
48
 
41
- // v2 dialect rules, worth internalizing once:
42
- // - link fields use ONE bare name both directions (no _ids suffix — that's v1)
43
- // - NEVER send a "world" field (identity comes from the key; the client strips it anyway)
49
+ ```typescript
44
50
  const location = await writer.create('location', {
45
51
  name: 'Dragon Peak',
46
- description: 'A treacherous mountain peak where dragons nest',
47
- }); // id minted client-side when omitted (retries stay idempotent)
48
- // name is the one required field, and required means the key is present:
49
- // '' and null are accepted and stored as '' (nameless Markers exist).
52
+ description: 'A mountain peak where dragons nest',
53
+ }); // the id is minted client-side when omitted, so a retry creates nothing twice
50
54
 
51
55
  const dragon = await writer.create('creature', {
52
56
  name: 'Vorrath the Ember-Scaled',
53
- location: location.id, // single link: UUID (or null)
57
+ location: location.id, // a single link: a UUID or null
54
58
  });
55
59
 
56
60
  const fetched = await writer.get('creature', dragon.id);
57
- // fetched.location === location.id — reads the way it writes
61
+ // fetched.location === location.id: a field reads the way it writes
58
62
 
59
- // PATCH is destructive on sent fields; arrays replace wholesale
63
+ // PATCH replaces the fields it sends; an array is replaced whole
60
64
  await writer.patch('location', location.id, { supertype: 'Mountain' });
61
65
 
62
- // For relationships, use the atomic link merge — returns the full updated element
63
- const fireBreath = await writer.create('ability', { name: 'Ember Breath' });
64
- await writer.editLinks('creature', dragon.id, 'abilities', { add: [fireBreath.id], remove: [] });
66
+ // For links, the atomic merge; it returns the updated element
67
+ const breath = await writer.create('ability', { name: 'Ember Breath' });
68
+ await writer.editLinks('creature', dragon.id, 'abilities', { add: [breath.id], remove: [] });
65
69
 
66
- // Walk every page of a type
70
+ // Every page of a type
67
71
  for await (const character of writer.listAll('character')) { /* ... */ }
68
72
  ```
69
73
 
70
- ## Bulk writes — always check `errors`
74
+ A link field has one name in both directions (`location`, not `location_id`). Only `name` is required, and an empty name is stored as `''`. A `world` field in a payload is ignored: the key decides the world, and the client strips it anyway.
75
+
76
+ ## Bulk writes
71
77
 
72
- `/bulk` **succeeds partially by default** (HTTP 200 with per-slot statuses). The one mistake to
73
- never make: treating a 200 as "everything landed."
78
+ `/bulk` answers 200 even when some items fail, so check `errors`:
74
79
 
75
80
  ```typescript
76
81
  const res = await writer.bulk(
@@ -78,87 +83,80 @@ const res = await writer.bulk(
78
83
  { type: 'character', element: { name: 'A' } },
79
84
  { type: 'event', element: { name: 'B' } },
80
85
  ],
81
- { idempotencyKey: crypto.randomUUID() }, // mint FRESH per attempt — a failed batch is
82
- ); // cached under its key; never reuse across retries
86
+ { idempotencyKey: crypto.randomUUID() }, // a new key per attempt: a failed batch is cached under its key
87
+ );
83
88
  if (res.errors) {
84
89
  for (const slot of res.items.filter((s) => s.status >= 400)) {
85
90
  console.warn(slot.error?.code, slot.error?.message, slot.error?.doc_url);
86
91
  }
87
92
  }
88
- // Pass { atomic: true } for all-or-nothing instead. res.wasReplay flags idempotent replays.
89
- // After a failed atomic batch NOTHING was written, but the slots that would have succeeded
90
- // still say 201 (keel's counterfactual 201s): do not record those ids as created.
91
93
  ```
92
94
 
95
+ Pass `{ atomic: true }` for all or nothing. After a failed atomic batch nothing was written, but the slots that would have succeeded still read 201: don't record those ids as created. `res.wasReplay` marks an idempotent replay.
96
+
93
97
  ## Images
94
98
 
95
99
  ```typescript
96
- const image = await writer.uploadImage(file); // a Blob, File, ArrayBuffer or Uint8Array
100
+ const image = await writer.uploadImage(file); // a Blob, File, ArrayBuffer or Uint8Array
97
101
  await writer.patch('character', id, { image_url: image.url });
98
102
  ```
99
103
 
100
- Two requests: a single-use ticket from keel, then the bytes straight to the edge (keel never
101
- sees them, and the edge never sees your key). webp, png, jpeg, avif or gif, read from the
102
- bytes; never SVG. Each ticket counts toward the world's daily limit and the account's image
103
- storage. To upload yourself (for a progress bar), take `createMediaTicket()` and POST the bytes to
104
- its `upload_url` with `Authorization: Bearer <ticket>`.
104
+ The SDK asks the API for a single-use ticket, then sends the bytes straight to the upload host, which never sees your key. webp, png, jpeg or avif (no SVG or gif), up to 15 MB. Each upload counts toward the world's daily limit and the account's storage. To upload yourself (for a progress bar), call `createMediaTicket()` and POST the bytes to its `upload_url` with `Authorization: Bearer <ticket>`.
105
105
 
106
106
  ## Sync
107
107
 
108
108
  ```typescript
109
- let cursor; // opaque, never expires; persist it
109
+ let cursor; // opaque and never expires: keep it
110
110
  for await (const change of client.changesAll(cursor)) {
111
- // ops arrive in (change_seq, id) order — apply in order → convergence
111
+ // changes arrive in order; apply them in order and you converge
112
112
  }
113
- // GOTCHA: world-meta edits (name, calendar, public_read) do NOT enter /changes.
114
- // Poll client.getWorld() and compare updated_at separately.
115
113
  ```
116
114
 
115
+ Edits to the world itself (its name, calendar, `public_read`) don't appear in the change feed. Read `client.getWorld()` and compare `updated_at`.
116
+
117
117
  ## Errors
118
118
 
119
- Every non-2xx throws `OwApiError` carrying the platform envelope: `.status`, `.type`, `.code`,
120
- `.param` (the exact field that failed), and `.docUrl` — surface `docUrl` in your UX. Transport
121
- failures throw `OwNetworkError`. `err.isValidationError` is the common branch.
119
+ Every non-2xx response throws `OwApiError` with the API's envelope: `.status`, `.type`, `.code`, `.param` (the field that failed) and `.docUrl`, which is worth showing to your users. A transport failure throws `OwNetworkError`. `err.isValidationError` covers the common case.
122
120
 
123
- ## Canonical constants (all generated or test-gated from schema)
121
+ ## Schema constants
124
122
 
125
123
  ```typescript
126
124
  import {
127
- ELEMENT_TYPES, // the 22 slugs (and the ElementType union)
128
- ELEMENT_ICONS, // Material Symbols icon per type
129
- ELEMENT_LABELS, // plural display labels
130
- ELEMENT_SECTIONS, // canonical field grouping + display order
131
- FIELD_SCHEMA, // per-field type/target metadata
132
- elementColor, // canonical colour: family carries COLOUR, icon carries TYPE
125
+ ELEMENT_TYPES, // the 22 type slugs (and the ElementType union)
126
+ ELEMENT_ICONS, // a Material Symbols icon per type
127
+ ELEMENT_LABELS, // plural display labels
128
+ ELEMENT_SECTIONS, // field grouping and display order
129
+ FIELD_SCHEMA, // per-field type and link target
130
+ elementColor, // colour by family; the icon carries the type
133
131
  } from '@onlyworlds/sdk';
134
132
 
135
- elementColor('character', 'dark'); // '#3987e5' — four CVD-validated families
136
- // Always pair colour with icon + label; colour alone is not accessible.
133
+ elementColor('character', 'dark'); // '#3987e5'
137
134
  ```
138
135
 
139
- The full schema with per-field meaning lives in [SCHEMA.md](SCHEMA.md) (generated, ships in this
140
- package). AI agents: read [AGENTS.md](AGENTS.md) first.
136
+ The four colour families (agents, world, abstract, temporal) are checked for colour-blind separation. Show colour with the icon and label, never alone. [SCHEMA.md](SCHEMA.md) (generated, in the package) describes every field; AI agents should read [AGENTS.md](AGENTS.md) first.
137
+
138
+ ## Tokens
141
139
 
142
- ## Token rating system
140
+ The account's token allowance and ratings, which some OnlyWorlds tools use.
143
141
 
144
142
  ```typescript
145
143
  import { TokenResource } from '@onlyworlds/sdk';
146
- const tokens = new TokenResource(writer); // OwV2Client.request() satisfies TokenTransport
144
+ const tokens = new TokenResource(writer);
147
145
  const status = await tokens.getStatus();
148
146
  ```
149
147
 
150
- ## AI access — when to use which
148
+ ## SDK or MCP
151
149
 
152
- For **known, deterministic operations** (CRUD, sync, bulk) use this SDK — typed calls, no
153
- tool-schema overhead. For **live exploration of a user's world from a chat/agent context**, use
154
- the MCP server at `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 11 tools;
155
- unaffected by SDK versioning).
150
+ Use this SDK for known operations in your own code (CRUD, sync, bulk). To let an AI assistant that speaks MCP (the Model Context Protocol) explore a world in a chat, connect it to `https://www.onlyworlds.com/mcp` with the same key and PIN as `API-Key` and `API-Pin` headers.
156
151
 
157
- ## License
152
+ ## Version 3 and the old API
158
153
 
159
- MIT
154
+ 4.x speaks the current (v2) API only. The v1 client (`OnlyWorldsClient`) and CommonJS `require()` stay on 3.x, which remains published and supported: see [the migration guide](docs/migrating-3-to-4.md).
160
155
 
161
156
  ## Links
162
157
 
163
- - [OnlyWorlds](https://www.onlyworlds.com) · [Docs](https://onlyworlds.github.io) · [API reference](https://www.onlyworlds.com/api/docs)
164
- - [Issues](https://github.com/OnlyWorlds/sdk/issues) · [CHANGELOG](CHANGELOG.md) · [Migration 3→4](docs/migrating-3-to-4.md)
158
+ [Docs](https://onlyworlds.github.io/docs/development/typescript) · [Games](https://onlyworlds.github.io/docs/development/games) · [API reference](https://www.onlyworlds.com/api/docs) · [Issues](https://github.com/OnlyWorlds/sdk/issues) · [Changelog](CHANGELOG.md)
159
+
160
+ ## License
161
+
162
+ MIT
package/SCHEMA.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OnlyWorlds Schema Reference
2
2
 
3
- **Source**: https://github.com/OnlyWorlds/schema-dist @ **v0.30.1-dist.15** — canonical schema **00.30.01**, published 2026-09-18.
3
+ **Source**: https://github.com/OnlyWorlds/schema-dist @ **v0.30.2-dist.16** — canonical schema **00.30.02**, published 2026-10-08.
4
4
 
5
5
  GENERATED from the canonical schema YAML — do not hand-edit (regenerate: `python codegen/generate_types.py`).
6
6
  Written for both humans and AI agents reading this package locally.
@@ -326,8 +326,8 @@ Families (colour semantics; icon carries the type): agents · world · abstract
326
326
  - `governing_title` (single link → title) — Governing figure assigned by the location's primary power
327
327
  - `secondary_powers` (multi link → institution) — Institutions with significant political control
328
328
  - `zone` (single link → zone) — Zone of interest that is associated with the location
329
- - `rival` (single link → location) — Locations with active, traditional, or historical rivalries
330
- - `partner` (single link → location) — Locations with active, cooperative, or historical ties
329
+ - `rival` (single link → location) — Location with an active, traditional, or historical rivalry with this one
330
+ - `partner` (single link → location) — Location with active, cooperative, or historical ties to this one
331
331
 
332
332
  ### World
333
333
 
@@ -388,7 +388,7 @@ Families (colour semantics; icon carries the type): agents · world · abstract
388
388
  - `x` (integer) — x coordinate, from bottom left of the map
389
389
  - `y` (integer) — y coordinate, from bottom left of the map
390
390
  - `z` (integer) — z coordinate, in case of depth
391
- - `order` (integer) — Sequence position when markers define a polygon or line (0 = first point)
391
+ - `order` (integer) — Sequence position when markers define a polygon or line (0 = first point); without it, markers keep the order they were made in
392
392
 
393
393
 
394
394
  ## narrative · family: temporal · icon: menu_book
@@ -482,7 +482,7 @@ Families (colour semantics; icon carries the type): agents · world · abstract
482
482
  ### Details
483
483
 
484
484
  - `map` (single link → map) — Map that the pin is placed on
485
- - `element` (generic link (any element type)) — Link to any Element (managed by ContentType + UUID)
485
+ - `element` (generic link (any element type)) — The element this pin places on the map, of any type
486
486
  - `x` (integer) — x coordinate, from bottom left of the map
487
487
  - `y` (integer) — y coordinate, from bottom left of the map
488
488
  - `z` (integer) — z coordinate, in case of depth (optional)
@@ -517,7 +517,6 @@ Families (colour semantics; icon carries the type): agents · world · abstract
517
517
  - `families` (multi link → family) — Families relevant to the relation
518
518
  - `titles` (multi link → title) — Titles relevant to the relation
519
519
  - `constructs` (multi link → construct) — Concepts, contracts, or principles relevant to the relation
520
- - `events` (multi link → event) — Events where the relation is involved or relevant
521
520
  - `narratives` (multi link → narrative) — Narratives relevant to the relation
522
521
 
523
522
 
package/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ type ElementType = 'ability' | 'character' | 'collective' | 'construct' | 'creat
33
33
  declare const ELEMENT_TYPES: ElementType[];
34
34
  /** Canonical OnlyWorlds schema version. Source: the `canonical:` value of the pinned
35
35
  * distribution's VERSION file (see the provenance block at the top of this file). */
36
- declare const ONLYWORLDS_VERSION: "00.30.01";
36
+ declare const ONLYWORLDS_VERSION: "00.30.02";
37
37
  /** The four semantic families (colour carries the family; ELEMENT_ICONS carries the type). */
38
38
  type ElementFamily = 'agents' | 'world' | 'abstract' | 'temporal';
39
39
  /** Per-type semantic family. Source: the distribution's `presentation.json` sidecar
@@ -1844,7 +1844,7 @@ declare const FIELD_SCHEMA: {
1844
1844
  };
1845
1845
 
1846
1846
  /**
1847
- * keel v2 engine absorbed from Assembly's ow-v2-client v0.9.0 (Kael),
1847
+ * keel v2 engine absorbed from an earlier v2 client (ow-v2-client v0.9.0),
1848
1848
  * wire-corrected against live staging fixtures 2026-07-18.
1849
1849
  *
1850
1850
  * OnlyWorlds v2 (keel) wire types -- the NON-generated, hand-owned wire shapes
@@ -2000,32 +2000,31 @@ interface ListParams {
2000
2000
  filter?: Record<string, string | number | boolean>;
2001
2001
  }
2002
2002
  interface OwClientConfig {
2003
- /** ow_w_ / ow_r_ / ow_a_ prefixed key, or grandfathered 10-digit legacy key. */
2003
+ /** The key: an ow_w_ (write) or ow_r_ (read) world key, an ow_a_ account token (sent as a Bearer token, for the account routes), or a 10-digit legacy key. */
2004
2004
  apiKey: string;
2005
2005
  /**
2006
- * Optional. Required for writes when the world has a PIN, and for legacy-key
2007
- * reads of private worlds. Prefixed keys read PIN-less. String, not number --
2008
- * '0123' !== 123.
2006
+ * The PIN, needed for writes when the world has one, and for legacy-key reads of private worlds.
2007
+ * Prefixed keys read without a PIN. A string, not a number: '0123' !== 123.
2009
2008
  */
2010
2009
  apiPin?: string;
2011
- /** Default: https://www.onlyworlds.com/api/v2 */
2010
+ /** The API's base URL, default https://www.onlyworlds.com/api/v2. */
2012
2011
  baseUrl?: string;
2013
2012
  /**
2014
- * Page size for element lists. Default 100 (server default; max 1000).
2015
- * Deliberately visible in config: page size is a citizenship property.
2013
+ * Page size for element lists, default 100 (the server's default; at most 1000).
2014
+ * It is in the config on purpose: page size decides the load a client puts on the server.
2016
2015
  */
2017
2016
  pageSize?: number;
2018
2017
  /**
2019
- * Page size for /changes pulls. Default 100. Live precedents: Obsidian 100,
2020
- * Atlas 250, MCP 25. /changes is the platform's heaviest route -- be polite.
2018
+ * Page size for /changes pulls, default 100. Tools in use pick 25 to 250; the change feed
2019
+ * is the platform's heaviest route, so keep it modest.
2021
2020
  */
2022
2021
  changesPageSize?: number;
2023
- /** Injectable for tests / fake-keel harnesses. Defaults to globalThis.fetch. */
2022
+ /** A fetch implementation to use instead of globalThis.fetch (for tests and other runtimes). */
2024
2023
  fetch?: typeof globalThis.fetch;
2025
2024
  }
2026
2025
 
2027
2026
  /**
2028
- * keel v2 engine absorbed from Assembly's ow-v2-client v0.9.0 (Kael),
2027
+ * keel v2 engine absorbed from an earlier v2 client (ow-v2-client v0.9.0),
2029
2028
  * wire-corrected against live staging fixtures 2026-07-18.
2030
2029
  *
2031
2030
  * keel error envelope handling. The error contract is part of the contract:
@@ -2079,6 +2078,13 @@ declare class OwApiError extends Error {
2079
2078
  * per-item slot `{status: 409, error: {code: 'id_conflict'}}` — check the slot.
2080
2079
  */
2081
2080
  get isIdConflict(): boolean;
2081
+ /**
2082
+ * `/changes` refused the cursor (409 `resync_required`): a guest's view of the world
2083
+ * changed, or the key's role did (a guest's cursor has another shape). Drop the stored
2084
+ * cursor, walk again from the start, and REPLACE the local copy rather than merging into
2085
+ * it: a guest never receives delete ops, so only the replace removes what it no longer sees.
2086
+ */
2087
+ get isResyncRequired(): boolean;
2082
2088
  /** keel's admission control turned the request away (503 `server_busy`); see retryAfter. */
2083
2089
  get isBusy(): boolean;
2084
2090
  /**
@@ -2109,7 +2115,7 @@ declare function parseErrorEnvelope(status: number, body: unknown, retryAfter?:
2109
2115
  declare function errorFromResponse(res: Response): Promise<OwApiError>;
2110
2116
 
2111
2117
  /**
2112
- * keel v2 engine absorbed from Assembly's ow-v2-client v0.9.0 (Kael),
2118
+ * keel v2 engine absorbed from an earlier v2 client (ow-v2-client v0.9.0),
2113
2119
  * wire-corrected against live staging fixtures 2026-07-18.
2114
2120
  *
2115
2121
  * OnlyWorlds key-kind detection. Prefixes make leaked keys grep-scannable
@@ -2137,7 +2143,7 @@ declare function kindCanWrite(kind: OwKeyKind): boolean;
2137
2143
  declare function pinExpectation(kind: OwKeyKind): 'never' | 'optional' | 'required-for-private-reads';
2138
2144
 
2139
2145
  /**
2140
- * keel v2 engine absorbed from Assembly's ow-v2-client v0.9.0 (Kael),
2146
+ * keel v2 engine absorbed from an earlier v2 client (ow-v2-client v0.9.0),
2141
2147
  * wire-corrected against live staging fixtures 2026-07-18.
2142
2148
  *
2143
2149
  * OwV2Client -- thin typed fetch client for the keel v2 API.
@@ -2175,11 +2181,11 @@ declare class OwV2Client {
2175
2181
  /** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
2176
2182
  get(type: ElementType | string, id: string, opts?: Pick<ListParams, 'expand' | 'fields'>): Promise<OwElement>;
2177
2183
  /**
2178
- * POST /{type}/ -- create. Mints an RFC 9562 UUIDv7 for element.id when the
2179
- * caller omits one (design ruling D29d) so a retry carrying the same
2180
- * Idempotency-Key is structurally safe. Callers MAY still supply their own id.
2184
+ * POST /{type}/ -- create one element, minting a UUIDv7 id on the client when you leave id out.
2185
+ * The client-side id makes a retry carrying the same Idempotency-Key safe: it rewrites
2186
+ * the same element instead of creating a second one. You may still supply your own id.
2181
2187
  * `name` is the one required field, and required means the key is present: `''` and
2182
- * `null` are accepted and stored as `''` (nameless Markers exist; keel ruling 2026-09-28).
2188
+ * `null` are accepted and stored as `''` (nameless Markers exist).
2183
2189
  */
2184
2190
  create(type: ElementType | string, element: OwElement | Record<string, unknown>, opts?: {
2185
2191
  idempotencyKey?: string;
@@ -2223,10 +2229,12 @@ declare class OwV2Client {
2223
2229
  }): Promise<OwBulkResponse>;
2224
2230
  /**
2225
2231
  * GET /changes -- one page of the world's ordered change feed.
2226
- * Cursor is OPAQUE and never expires: persist verbatim, never parse.
2232
+ * Cursor is OPAQUE: persist verbatim, never parse or build one (a guest's has
2233
+ * another shape than everyone else's).
2227
2234
  * Zero/absent cursor = full export (byte-aligned with the Folder Format).
2228
- * Rewind rule: if your persisted position is ahead of page.head, the server
2229
- * was restored -- re-baseline from cursor zero; do not assume caught-up.
2235
+ * Reset rule: a 409 `resync_required` (err.isResyncRequired) means the key's
2236
+ * view changed (a guest's, or its role): drop the cursor, walk from zero, and
2237
+ * replace the local copy rather than merging into it.
2230
2238
  * Citizenship: heaviest route on the platform; default page size is polite.
2231
2239
  */
2232
2240
  changes(opts?: {
@@ -2254,12 +2262,12 @@ declare class OwV2Client {
2254
2262
  */
2255
2263
  createMediaTicket(): Promise<OwMediaTicket>;
2256
2264
  /**
2257
- * Upload one image and get its permanent public URL: a ticket from keel, then the
2258
- * bytes straight to the edge (keel never sees them). Pass a ticket from
2265
+ * Upload one image and get its permanent public URL: a ticket from the API, then the
2266
+ * bytes straight to the media edge (the API never sees them). Pass a ticket from
2259
2267
  * createMediaTicket() to use it instead of fetching one. Returns the edge's 201;
2260
2268
  * set `url` as an element's `image_url` yourself (`patch(type, id, { image_url })`).
2261
2269
  *
2262
- * webp, png, jpeg, avif or gif, read from the bytes (never SVG). `key` names the
2270
+ * webp, png, jpeg or avif, read from the bytes (never SVG or gif). `key` names the
2263
2271
  * object (it must start with the ticket's `prefix`; without it the edge picks one).
2264
2272
  * An image larger than the ticket's `max_bytes` throws 413 `too_large` before any
2265
2273
  * byte is sent. The edge's refusals arrive as OwApiError with its code: 401
@@ -2292,9 +2300,8 @@ declare class OwV2Client {
2292
2300
  /**
2293
2301
  * Canonical element colour palette — four semantic families.
2294
2302
  *
2295
- * Ruled by Captain 2026-07-22 after Skeld's measurement pass (Orrery
2296
- * `product/schema/element-palette-measurements.md`): 22 mutually-separable
2297
- * hues is structurally impossible; four families is the ceiling that passes
2303
+ * Ruled 2026-07-22 after a measurement pass: 22 mutually-separable hues
2304
+ * is structurally impossible; four families is the ceiling that passes
2298
2305
  * all-pairs CVD separation in both modes. **Colour carries the FAMILY; the
2299
2306
  * icon (`ELEMENT_ICONS`) carries the TYPE.** Dark-mode pairs land in the 6–8
2300
2307
  * CVD floor band, so secondary encoding (icon + label) is REQUIRED alongside
@@ -2485,7 +2492,7 @@ declare enum GameTier {
2485
2492
  /**
2486
2493
  * Minimal transport the token resource needs — structurally satisfied by
2487
2494
  * `OwV2Client` (its `request()` is public since 4.0): `new TokenResource(client)`.
2488
- * Wire ruling (Skeld 2026-07-23): keel keeps the /tokens/* economy long-term
2495
+ * Wire ruling (2026-07-23): keel keeps the /tokens/* economy long-term
2489
2496
  * (Tangle ratings write it, Council voting reads it) — port, don't delete.
2490
2497
  */
2491
2498
  interface TokenTransport {
package/dist/index.js CHANGED
@@ -35,6 +35,15 @@ var OwApiError = class extends Error {
35
35
  get isIdConflict() {
36
36
  return this.status === 409 && this.code === "id_conflict";
37
37
  }
38
+ /**
39
+ * `/changes` refused the cursor (409 `resync_required`): a guest's view of the world
40
+ * changed, or the key's role did (a guest's cursor has another shape). Drop the stored
41
+ * cursor, walk again from the start, and REPLACE the local copy rather than merging into
42
+ * it: a guest never receives delete ops, so only the replace removes what it no longer sees.
43
+ */
44
+ get isResyncRequired() {
45
+ return this.status === 409 && this.code === "resync_required";
46
+ }
38
47
  /** keel's admission control turned the request away (503 `server_busy`); see retryAfter. */
39
48
  get isBusy() {
40
49
  return this.status === 503 && this.code === "server_busy";
@@ -188,11 +197,11 @@ var OwV2Client = class {
188
197
  return this.request("GET", `/${type}/${id}/`, { query });
189
198
  }
190
199
  /**
191
- * POST /{type}/ -- create. Mints an RFC 9562 UUIDv7 for element.id when the
192
- * caller omits one (design ruling D29d) so a retry carrying the same
193
- * Idempotency-Key is structurally safe. Callers MAY still supply their own id.
200
+ * POST /{type}/ -- create one element, minting a UUIDv7 id on the client when you leave id out.
201
+ * The client-side id makes a retry carrying the same Idempotency-Key safe: it rewrites
202
+ * the same element instead of creating a second one. You may still supply your own id.
194
203
  * `name` is the one required field, and required means the key is present: `''` and
195
- * `null` are accepted and stored as `''` (nameless Markers exist; keel ruling 2026-09-28).
204
+ * `null` are accepted and stored as `''` (nameless Markers exist).
196
205
  */
197
206
  async create(type, element, opts = {}) {
198
207
  const body = sanitizePayload(element);
@@ -260,10 +269,12 @@ var OwV2Client = class {
260
269
  // -- Changes feed --------------------------------------------------------
261
270
  /**
262
271
  * GET /changes -- one page of the world's ordered change feed.
263
- * Cursor is OPAQUE and never expires: persist verbatim, never parse.
272
+ * Cursor is OPAQUE: persist verbatim, never parse or build one (a guest's has
273
+ * another shape than everyone else's).
264
274
  * Zero/absent cursor = full export (byte-aligned with the Folder Format).
265
- * Rewind rule: if your persisted position is ahead of page.head, the server
266
- * was restored -- re-baseline from cursor zero; do not assume caught-up.
275
+ * Reset rule: a 409 `resync_required` (err.isResyncRequired) means the key's
276
+ * view changed (a guest's, or its role): drop the cursor, walk from zero, and
277
+ * replace the local copy rather than merging into it.
267
278
  * Citizenship: heaviest route on the platform; default page size is polite.
268
279
  */
269
280
  async changes(opts = {}) {
@@ -300,12 +311,12 @@ var OwV2Client = class {
300
311
  return this.request("POST", "/media/ticket");
301
312
  }
302
313
  /**
303
- * Upload one image and get its permanent public URL: a ticket from keel, then the
304
- * bytes straight to the edge (keel never sees them). Pass a ticket from
314
+ * Upload one image and get its permanent public URL: a ticket from the API, then the
315
+ * bytes straight to the media edge (the API never sees them). Pass a ticket from
305
316
  * createMediaTicket() to use it instead of fetching one. Returns the edge's 201;
306
317
  * set `url` as an element's `image_url` yourself (`patch(type, id, { image_url })`).
307
318
  *
308
- * webp, png, jpeg, avif or gif, read from the bytes (never SVG). `key` names the
319
+ * webp, png, jpeg or avif, read from the bytes (never SVG or gif). `key` names the
309
320
  * object (it must start with the ticket's `prefix`; without it the edge picks one).
310
321
  * An image larger than the ticket's `max_bytes` throws 413 `too_large` before any
311
322
  * byte is sent. The edge's refusals arrive as OwApiError with its code: 401
@@ -422,7 +433,7 @@ function buildQuery(params) {
422
433
 
423
434
  // src/v2/types.generated.ts
424
435
  var ELEMENT_TYPES = ["ability", "character", "collective", "construct", "creature", "event", "family", "institution", "language", "law", "location", "map", "marker", "narrative", "object", "phenomenon", "pin", "relation", "species", "title", "trait", "zone"];
425
- var ONLYWORLDS_VERSION = "00.30.01";
436
+ var ONLYWORLDS_VERSION = "00.30.02";
426
437
  var ELEMENT_FAMILIES = {
427
438
  ability: "abstract",
428
439
  character: "agents",
@@ -553,7 +564,7 @@ var ELEMENT_SECTIONS = {
553
564
  ],
554
565
  relation: [
555
566
  { name: "Nature", order: 1, fields: ["background", "start_date", "end_date", "intensity", "actor", "events"] },
556
- { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "titles", "constructs", "events", "narratives"] }
567
+ { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "titles", "constructs", "narratives"] }
557
568
  ],
558
569
  species: [
559
570
  { name: "Biology", order: 1, fields: ["appearance", "life_span", "weight", "nourishment", "reproduction", "adaptations"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.6.0",
3
+ "version": "4.7.1",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {