@onlyworlds/sdk 4.7.3 → 4.8.0

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,62 +1,63 @@
1
- # For AI agents using @onlyworlds/sdk
2
-
3
- **Current as of**: SDK **4.x** · schema-dist **v0.31.0-dist.19** (canonical 00.31.00).
4
- This line is asserted by `codegen:check` in CI — if the pin moves and this file is not
5
- re-read against it, the check fails rather than letting this document rot quietly.
6
-
7
- **Start with [SCHEMA.md](SCHEMA.md)** (in this package): the full generated schema reference —
8
- every type, every field with its meaning, link directions, families, icons, display sections.
9
- It is generated from the same canonical YAML as the types, so it cannot drift.
10
-
11
- **What this package is**: the canonical typed TypeScript client for the OnlyWorlds v2 API,
12
- plus the canonical constants (element types, icons, colour families, field schema).
13
- OnlyWorlds is an open standard for portable world data — 22 element types, UUID-linked.
14
-
15
- **Use the v2 surface.** `OwV2Client` + the `V2ElementType` slug union + the generated
16
- interfaces in `types.generated.ts` (emitted from the canonical schema YAML, validated
17
- against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum) is not in
18
- 4.x — it was removed at 4.0.0 and lives only in 3.x. Do not build new work on it.
19
-
20
- **SDK vs MCP server — pick correctly**:
21
- - Known, deterministic operations (CRUD, sync, bulk) → **this SDK**. Typed calls, typed
22
- responses, far cheaper than tool-schema reasoning.
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, 12 tools).
25
-
26
- **Wire facts that bite** (full details in README):
27
- - Never send a `"world"` field in payloads — world identity comes from the API key (422 otherwise).
28
- - v2 link fields use ONE name both directions (no `_ids` suffix — that is v1 dialect only).
29
- - PATCH is destructive on sent fields; use `editLinks` (atomic add/remove) for relationships.
30
- - World-meta changes do NOT appear in `/changes` — poll `GET /world` separately.
31
- - Extension fields: `x_<toolname>_*` is the sanctioned namespace for tool-specific state;
32
- unknown unprefixed fields 422. Extensions are capped at **64 KB per element** (422,
33
- `param: extensions`).
34
- - List filters: `name` (exact), `name__icontains`, `supertype`, `subtype`, and
35
- `characters=<id>` on types with a `characters` link. Any other filter key — and
36
- `?ordering=` — returns 422 naming the accepted ones. Sort client-side.
37
- - Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
38
- to the edge with a single-use ticket from keel, never with the API key.
39
- - Ids: the client mints **UUIDv7** on an id-less `create` (since 4.2.0; keel mints v7 too).
40
- A v4 or v7 id you supply is accepted. **Never sort elements by id**: worlds mix v7, v4
41
- and legacy `06x…` ids (nibble 7 too, but seconds-first). For creation order use
42
- `created_at`; `change_seq` is last-write order, not creation.
43
- A create with an id that already exists, or a PUT whose id belongs to **another world**,
44
- returns 409 `id_conflict` (`err.isIdConflict`; retrying won't help). `/bulk` never throws:
45
- check each slot for `status: 409` with `error.code: 'id_conflict'`. A different 409, `idempotency_error`, means an
46
- Idempotency-Key was reused with another body (`err.isIdempotencyConflict`).
47
- - Under load keel answers 503 `server_busy` with `Retry-After` (`err.isBusy`, `err.retryAfter`
48
- in seconds). The client does not retry for you; back off and retry yourself. Works the same
49
- in browsers.
50
- - `created_by` rides every element read: the id of the membership that created it, `null` for
51
- the world's owner and for anything made before memberships existed. It is read-only; a write
52
- body that carries it has it dropped, never an error. A **contributor** can change only what
53
- they created: anything else is 403 `not_author` (`err.isNotAuthor`; retrying will not help).
54
- `/bulk` reports it per slot, not as a throw.
55
- - A string holding an unpaired surrogate (text cut mid-emoji) is a 422 naming the field.
56
- Slice strings by code point, not by UTF-16 unit.
57
- - Colour carries the element's FAMILY (`elementColor(type, mode)`); the icon
58
- (`ELEMENT_ICONS`) carries the TYPE. Icon + label are required alongside colour, not optional.
59
-
60
- **Auth**: prefixed keys — `ow_w_` (read+write), `ow_r_` (read-only, no PIN — the share
61
- primitive), `ow_a_` (account Bearer). Demo keys `0000000000`–`0000000009` are read-only
62
- test credentials against real data.
1
+ # For AI agents using @onlyworlds/sdk
2
+
3
+ **Current as of**: SDK **4.x** · schema-dist **v0.31.0-dist.19** (canonical 00.31.00).
4
+ This line is asserted by `codegen:check` in CI — if the pin moves and this file is not
5
+ re-read against it, the check fails rather than letting this document rot quietly.
6
+
7
+ **Start with [SCHEMA.md](SCHEMA.md)** (in this package): the full generated schema reference —
8
+ every type, every field with its meaning, link directions, families, icons, display sections.
9
+ It is generated from the same canonical YAML as the types, so it cannot drift.
10
+
11
+ **What this package is**: the canonical typed TypeScript client for the OnlyWorlds v2 API,
12
+ plus the canonical constants (element types, icons, colour families, field schema).
13
+ OnlyWorlds is an open standard for portable world data — 22 element types, UUID-linked.
14
+
15
+ **Use the v2 surface.** `OwV2Client` + the `V2ElementType` slug union + the generated
16
+ interfaces in `types.generated.ts` (emitted from the canonical schema YAML, validated
17
+ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum) is not in
18
+ 4.x — it was removed at 4.0.0 and lives only in 3.x. Do not build new work on it.
19
+
20
+ **SDK vs MCP server — pick correctly**:
21
+ - Known, deterministic operations (CRUD, sync, bulk) → **this SDK**. Typed calls, typed
22
+ responses, far cheaper than tool-schema reasoning.
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, 12 tools).
25
+
26
+ **Wire facts that bite** (full details in README):
27
+ - Never send a `"world"` field in payloads — world identity comes from the API key (422 otherwise).
28
+ - v2 link fields use ONE name both directions (no `_ids` suffix — that is v1 dialect only).
29
+ - PATCH is destructive on sent fields; use `editLinks` (atomic add/remove) for relationships.
30
+ - World-meta changes do NOT appear in `/changes` — poll `GET /world` separately.
31
+ - Extension fields: `x_<toolname>_*` is the sanctioned namespace for tool-specific state;
32
+ unknown unprefixed fields 422. Extensions are capped at **64 KB per element** (422,
33
+ `param: extensions`).
34
+ - List filters: `name` (exact), `name__icontains`, `supertype`, `subtype`, and
35
+ `characters=<id>` on types with a `characters` link. Any other filter key — and
36
+ `?ordering=` — returns 422 naming the accepted ones. Sort client-side.
37
+ - Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
38
+ to the edge with a single-use ticket from keel, never with the API key. `removeImage(urlOrKey)`
39
+ takes one back (its uploader or the world's owner); it leaves `image_url` on elements as is.
40
+ - Ids: the client mints **UUIDv7** on an id-less `create` (since 4.2.0; keel mints v7 too).
41
+ A v4 or v7 id you supply is accepted. **Never sort elements by id**: worlds mix v7, v4
42
+ and legacy `06x…` ids (nibble 7 too, but seconds-first). For creation order use
43
+ `created_at`; `change_seq` is last-write order, not creation.
44
+ A create with an id that already exists, or a PUT whose id belongs to **another world**,
45
+ returns 409 `id_conflict` (`err.isIdConflict`; retrying won't help). `/bulk` never throws:
46
+ check each slot for `status: 409` with `error.code: 'id_conflict'`. A different 409, `idempotency_error`, means an
47
+ Idempotency-Key was reused with another body (`err.isIdempotencyConflict`).
48
+ - Under load keel answers 503 `server_busy` with `Retry-After` (`err.isBusy`, `err.retryAfter`
49
+ in seconds). The client does not retry for you; back off and retry yourself. Works the same
50
+ in browsers.
51
+ - `created_by` rides every element read: the id of the membership that created it, `null` for
52
+ the world's owner and for anything made before memberships existed. It is read-only; a write
53
+ body that carries it has it dropped, never an error. A **contributor** can change only what
54
+ they created: anything else is 403 `not_author` (`err.isNotAuthor`; retrying will not help).
55
+ `/bulk` reports it per slot, not as a throw.
56
+ - A string holding an unpaired surrogate (text cut mid-emoji) is a 422 naming the field.
57
+ Slice strings by code point, not by UTF-16 unit.
58
+ - Colour carries the element's FAMILY (`elementColor(type, mode)`); the icon
59
+ (`ELEMENT_ICONS`) carries the TYPE. Icon + label are required alongside colour, not optional.
60
+
61
+ **Auth**: prefixed keys — `ow_w_` (read+write), `ow_r_` (read-only, no PIN — the share
62
+ primitive), `ow_a_` (account Bearer). Demo keys `0000000000`–`0000000009` are read-only
63
+ test credentials against real data.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.8.0] — 2026-10-10
9
+
10
+ ### Added
11
+ - **`removeImage(keyOrUrl, { ticket? })`**: removes one uploaded image. A removal ticket from
12
+ keel (`POST /media/remove-ticket`, keel D91), then the removal at the ticket's `remove_url`
13
+ with the ticket only, never the API key. Takes the object key or the whole `image_url`. The
14
+ image's uploader or the world's owner key may remove it; the bytes go back to the uploader's
15
+ storage. Elements keep their `image_url`: clear it yourself.
16
+ - **`createRemovalTicket(key)`** and the types **`OwRemovalTicket`** (`ticket`, `remove_url`,
17
+ `exp` in unix seconds, `key`, `referenced`: how many elements still show the image) and
18
+ **`OwRemovedImage`** (`removed`, `bytes`, `cache`). Refusals arrive as `OwApiError`: keel's
19
+ `permission_error`, `not_found`, `removal_in_flight` and `upload_pending` (both 409 with
20
+ Retry-After); the edge's `ticket_expired`, `ticket_used`, `ticket_invalid`, `not_removable`,
21
+ `remove_failed`.
22
+
8
23
  ## [4.7.3] — 2026-10-10
9
24
 
10
25
  ### Changed
package/README.md CHANGED
@@ -101,6 +101,13 @@ await writer.patch('character', id, { image_url: image.url });
101
101
 
102
102
  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>`.
103
103
 
104
+ ```typescript
105
+ await writer.removeImage(image.url); // the image_url, or its object key
106
+ await writer.patch('character', id, { image_url: null });
107
+ ```
108
+
109
+ The image's uploader or the world's owner can remove an uploaded image; the space goes back to the uploader's storage. Removal leaves elements as they are, so clear their `image_url` yourself (`createRemovalTicket(key).referenced` says how many still show it). A copy the network already holds can keep answering for a while.
110
+
104
111
  ## Sync
105
112
 
106
113
  ```typescript
package/dist/index.d.ts CHANGED
@@ -1970,6 +1970,37 @@ interface OwMediaTicket {
1970
1970
  membership: string | null;
1971
1971
  };
1972
1972
  }
1973
+ /**
1974
+ * POST /media/remove-ticket's 200: a short-lived, single-use permission to remove one
1975
+ * image. keel issues it to the image's uploader or the world's owner key; the edge at
1976
+ * `remove_url` does the removal.
1977
+ */
1978
+ interface OwRemovalTicket {
1979
+ /** Opaque: send it to `remove_url` as `Authorization: Bearer <ticket>`, as is. */
1980
+ ticket: string;
1981
+ /** Where the removal goes: the upload host's /v1/remove (https://upload.onlyworlds.com/v1/remove on production). */
1982
+ remove_url: string;
1983
+ /** Unix seconds after which the edge refuses the ticket (10 minutes after issue). */
1984
+ exp: number;
1985
+ /** The object key the ticket removes. */
1986
+ key: string;
1987
+ /** How many of the world's elements show the image. Removal does not change them: clear their `image_url` yourself. */
1988
+ referenced: number;
1989
+ }
1990
+ /** The edge's 200 for one removal. */
1991
+ interface OwRemovedImage {
1992
+ /** The object key that was removed. */
1993
+ removed: string;
1994
+ /** The bytes given back to the uploading account's storage. */
1995
+ bytes: number;
1996
+ /**
1997
+ * `may_linger`: copies the network or a browser already holds can keep answering for days
1998
+ * or longer (images are cached as unchanging for a year). `purging`: the edge cache is
1999
+ * being cleared too.
2000
+ */
2001
+ cache: 'may_linger' | 'purging' | (string & {});
2002
+ [k: string]: unknown;
2003
+ }
1973
2004
  /** The edge's 201 for one upload. Put `url` in an element's `image_url`. */
1974
2005
  interface OwUploadedImage {
1975
2006
  /** The permanent public URL (https://media.onlyworlds.com/<key>). */
@@ -2280,6 +2311,34 @@ declare class OwV2Client {
2280
2311
  key?: string;
2281
2312
  ticket?: OwMediaTicket;
2282
2313
  }): Promise<OwUploadedImage>;
2314
+ /**
2315
+ * POST /media/remove-ticket -- permission to remove ONE image (a write key and its
2316
+ * PIN). The image's uploader may remove it, and so may the world's owner key; an
2317
+ * agent seat answers as its sponsor. Only images uploaded with a ticket qualify.
2318
+ * `key` is the object key, or the whole image_url (the part after the host is taken).
2319
+ * The reply's `referenced` counts the world's elements still showing the image.
2320
+ * Refusals: 403 `permission_error` (neither uploader nor owner), 404 `not_found`
2321
+ * (no removable image under this key), 409 `removal_in_flight` (a ticket for this
2322
+ * image is already out) and 409 `upload_pending` (an upload is not yet recorded), both
2323
+ * with Retry-After.
2324
+ */
2325
+ createRemovalTicket(key: string): Promise<OwRemovalTicket>;
2326
+ /**
2327
+ * Remove one uploaded image: a removal ticket from the API, then the removal at the
2328
+ * edge (the API key never leaves for the edge). Pass the object key or the whole
2329
+ * image_url, or a ticket from createRemovalTicket() to use it instead of fetching one.
2330
+ * The bytes go back to the uploading account's storage.
2331
+ *
2332
+ * Removal does not touch elements: clear or replace their `image_url` yourself (the
2333
+ * ticket's `referenced` says how many there are). A copy already cached by the network
2334
+ * or a browser can keep answering for a while (`cache: 'may_linger'`). The edge's
2335
+ * refusals arrive as OwApiError with its code: 401 `ticket_expired` / `ticket_used` /
2336
+ * `ticket_invalid`, 403 `not_removable`, 502 `remove_failed` (nothing removed; the
2337
+ * ticket is still unspent).
2338
+ */
2339
+ removeImage(keyOrUrl: string, opts?: {
2340
+ ticket?: OwRemovalTicket;
2341
+ }): Promise<OwRemovedImage>;
2283
2342
  /**
2284
2343
  * Raw authenticated request against this client's baseUrl. Public since 4.0
2285
2344
  * so auxiliary resources can ride the same transport — it structurally
@@ -2653,4 +2712,4 @@ declare class TokenResource {
2653
2712
  getEncryptionInfo(): Promise<EncryptionInfo>;
2654
2713
  }
2655
2714
 
2656
- export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwKeyKind, type OwLinkEdit, type OwMediaTicket, OwNetworkError, type OwPage, type OwUploadedImage, OwV2Client, type OwWorldMeta, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, parseErrorEnvelope, pinExpectation };
2715
+ export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwKeyKind, type OwLinkEdit, type OwMediaTicket, OwNetworkError, type OwPage, type OwRemovalTicket, type OwRemovedImage, type OwUploadedImage, OwV2Client, type OwWorldMeta, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, parseErrorEnvelope, pinExpectation };
package/dist/index.js CHANGED
@@ -348,6 +348,44 @@ var OwV2Client = class {
348
348
  if (!res.ok) throw await errorFromResponse(res);
349
349
  return await res.json();
350
350
  }
351
+ /**
352
+ * POST /media/remove-ticket -- permission to remove ONE image (a write key and its
353
+ * PIN). The image's uploader may remove it, and so may the world's owner key; an
354
+ * agent seat answers as its sponsor. Only images uploaded with a ticket qualify.
355
+ * `key` is the object key, or the whole image_url (the part after the host is taken).
356
+ * The reply's `referenced` counts the world's elements still showing the image.
357
+ * Refusals: 403 `permission_error` (neither uploader nor owner), 404 `not_found`
358
+ * (no removable image under this key), 409 `removal_in_flight` (a ticket for this
359
+ * image is already out) and 409 `upload_pending` (an upload is not yet recorded), both
360
+ * with Retry-After.
361
+ */
362
+ async createRemovalTicket(key) {
363
+ return this.request("POST", "/media/remove-ticket", { body: { key: objectKey(key) } });
364
+ }
365
+ /**
366
+ * Remove one uploaded image: a removal ticket from the API, then the removal at the
367
+ * edge (the API key never leaves for the edge). Pass the object key or the whole
368
+ * image_url, or a ticket from createRemovalTicket() to use it instead of fetching one.
369
+ * The bytes go back to the uploading account's storage.
370
+ *
371
+ * Removal does not touch elements: clear or replace their `image_url` yourself (the
372
+ * ticket's `referenced` says how many there are). A copy already cached by the network
373
+ * or a browser can keep answering for a while (`cache: 'may_linger'`). The edge's
374
+ * refusals arrive as OwApiError with its code: 401 `ticket_expired` / `ticket_used` /
375
+ * `ticket_invalid`, 403 `not_removable`, 502 `remove_failed` (nothing removed; the
376
+ * ticket is still unspent).
377
+ */
378
+ async removeImage(keyOrUrl, opts = {}) {
379
+ const ticket = opts.ticket ?? await this.createRemovalTicket(keyOrUrl);
380
+ let res;
381
+ try {
382
+ res = await this.fetchImpl(ticket.remove_url, { method: "POST", headers: { Authorization: `Bearer ${ticket.ticket}` } });
383
+ } catch (cause) {
384
+ throw new OwNetworkError(`OnlyWorlds image removal failed: POST ${ticket.remove_url}`, cause);
385
+ }
386
+ if (!res.ok) throw await errorFromResponse(res);
387
+ return await res.json();
388
+ }
351
389
  // -- Core request machinery ----------------------------------------------
352
390
  /**
353
391
  * Raw authenticated request against this client's baseUrl. Public since 4.0
@@ -423,6 +461,10 @@ function mintUuid(now = Date.now()) {
423
461
  for (let i = 0; i < 256; i++) hex.push((i + 256).toString(16).slice(1));
424
462
  return hex[bytes[0]] + hex[bytes[1]] + hex[bytes[2]] + hex[bytes[3]] + "-" + hex[bytes[4]] + hex[bytes[5]] + "-" + hex[bytes[6]] + hex[bytes[7]] + "-" + hex[bytes[8]] + hex[bytes[9]] + "-" + hex[bytes[10]] + hex[bytes[11]] + hex[bytes[12]] + hex[bytes[13]] + hex[bytes[14]] + hex[bytes[15]];
425
463
  }
464
+ function objectKey(keyOrUrl) {
465
+ if (!/^https?:\/\//i.test(keyOrUrl)) return keyOrUrl;
466
+ return decodeURIComponent(new URL(keyOrUrl).pathname.replace(/^\/+/, ""));
467
+ }
426
468
  function buildQuery(params) {
427
469
  const q = new URLSearchParams();
428
470
  for (const [k, v] of Object.entries(params)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.7.3",
3
+ "version": "4.8.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {