@onlyworlds/sdk 4.5.0 → 4.7.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
@@ -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).
@@ -31,8 +31,11 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
31
31
  - Extension fields: `x_<toolname>_*` is the sanctioned namespace for tool-specific state;
32
32
  unknown unprefixed fields 422. Extensions are capped at **64 KB per element** (422,
33
33
  `param: extensions`).
34
- - List filters: only `name__icontains`, `supertype` and `subtype` are built. Any other
35
- filter key — and `?ordering=` — returns 422 naming it. Filter or sort client-side.
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.
36
39
  - Ids: the client mints **UUIDv7** on an id-less `create` (since 4.2.0; keel mints v7 too).
37
40
  A v4 or v7 id you supply is accepted. **Never sort elements by id**: worlds mix v7, v4
38
41
  and legacy `06x…` ids (nibble 7 too, but seconds-first). For creation order use
package/CHANGELOG.md CHANGED
@@ -5,7 +5,60 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
- ## [4.5.0] — 2026-10-04 (staged, not published)
8
+ ## [4.7.0] — 2026-10-08
9
+
10
+ ### Added
11
+ - **`OwApiError.isResyncRequired`**: 409 `resync_required` from `/changes`. Guests can follow the
12
+ feed since keel D79; when a guest's view or a key's role changes, its stored cursor is refused. Drop
13
+ it, walk from zero, and replace the local copy rather than merging.
14
+
15
+ ### Documented
16
+ - `uploadImage` accepts webp, png, jpeg and avif. 4.6.0's JSDoc and README also listed gif, which
17
+ the edge refuses (415 `unsupported_type`); checked against the Worker's type table.
18
+ - `changes()`: the old rewind rule compared a stored cursor with `head`, which an opaque cursor
19
+ cannot do; replaced with the reset rule (`isResyncRequired`).
20
+ - The doc comments of `OwClientConfig`, `create` and `uploadImage` are written for users: internal
21
+ ruling numbers and test-harness notes are gone, and each opens with what the option or method does.
22
+ The docs site's TypeScript reference is generated from these comments.
23
+
24
+ ## [4.6.0] — 2026-10-06
25
+
26
+ Published from `e198298` (tag `v4.6.0`); the tarball was checked against a build of that commit.
27
+
28
+ Includes 4.5.1's documentation fixes: 4.5.1 was staged and never published.
29
+
30
+ ### Added
31
+ - **`uploadImage(image, { key?, ticket? })`**: upload one image (webp, png, jpeg, avif;
32
+ 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
33
+ (`POST /media/ticket`), then the bytes straight to the edge at the ticket's `upload_url`
34
+ (keel never sees them). Only the ticket goes to the edge, never the API key or PIN. An image
35
+ larger than the ticket's `max_bytes` throws 413 `too_large` before anything is sent; the edge's
36
+ refusals (`ticket_used`, `ticket_expired`, `bad_key`, `exists`, `unsupported_type`, ...) arrive
37
+ as `OwApiError` with their code. Set the returned `url` as an element's `image_url` yourself.
38
+ Checked live on 2026-10-06: one upload through keel's ticket to the edge (201), and the same
39
+ ticket again refused with 401 `ticket_used` (Keel #59).
40
+ - **`createMediaTicket()`**: the first step alone, for callers that upload themselves (progress
41
+ bars, say). Types `OwMediaTicket` and `OwUploadedImage`.
42
+
43
+ ### Documented
44
+ - `ListParams.filter` lists what keel accepts now: exact `name`, and `characters=<id>` on every
45
+ type with a `characters` link (keel D76d).
46
+
47
+ ## [4.5.1] — 2026-10-05 (never published; shipped in 4.6.0)
48
+
49
+ Docs only: no code path changes.
50
+
51
+ ### Documented
52
+ - **`isOwnerOnly`'s JSDoc in 4.5.0 was wrong about who sees `owner_only`.** It said owners and
53
+ co-builders never see it. A member key, a co-builder's too, gets it on `PATCH /world`; only the
54
+ owner's key is exempt (keel spec section 9; confirmed from keel's `patch_world` by Skeld,
55
+ 2026-10-04).
56
+ - **`world_gone` is reserved.** `OwAuthErrorCode` and `isAuthError` still carry it, but keel never
57
+ emits it: a deleted world's keys are deleted with it, so the key answers 401 `invalid_credentials`
58
+ (Skeld, 2026-10-04; keel spec section 2, T6b). Found by the wire gate reading the SDK's own
59
+ built file for the codes it keys on.
60
+
61
+ ## [4.5.0] — 2026-10-04
9
62
 
10
63
  What the staging write probes found on the wire (Python package, 2026-10-04): one new getter, and
11
64
  three behaviours the server has always had that no client doc said. Additive; nothing is removed.
package/README.md CHANGED
@@ -90,6 +90,19 @@ if (res.errors) {
90
90
  // still say 201 (keel's counterfactual 201s): do not record those ids as created.
91
91
  ```
92
92
 
93
+ ## Images
94
+
95
+ ```typescript
96
+ const image = await writer.uploadImage(file); // a Blob, File, ArrayBuffer or Uint8Array
97
+ await writer.patch('character', id, { image_url: image.url });
98
+ ```
99
+
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 or avif (never SVG or gif), read from the
102
+ bytes. 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>`.
105
+
93
106
  ## Sync
94
107
 
95
108
  ```typescript
@@ -138,7 +151,7 @@ const status = await tokens.getStatus();
138
151
 
139
152
  For **known, deterministic operations** (CRUD, sync, bulk) use this SDK — typed calls, no
140
153
  tool-schema overhead. For **live exploration of a user's world from a chat/agent context**, use
141
- the MCP server at `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 11 tools;
154
+ the MCP server at `https://www.onlyworlds.com/mcp` (same `API-Key`/`API-Pin` headers, 12 tools;
142
155
  unaffected by SDK versioning).
143
156
 
144
157
  ## License
package/dist/index.d.ts CHANGED
@@ -1947,6 +1947,41 @@ interface OwLinkEdit {
1947
1947
  add?: string[];
1948
1948
  remove?: string[];
1949
1949
  }
1950
+ /**
1951
+ * POST /media/ticket's 201: a short-lived, single-use permission to upload one image
1952
+ * into this world's prefix. keel never sees the bytes; the edge at `upload_url` does.
1953
+ */
1954
+ interface OwMediaTicket {
1955
+ /** Opaque: send it to `upload_url` as `Authorization: Bearer <ticket>`, as is. */
1956
+ ticket: string;
1957
+ /** Where the image goes (https://upload.onlyworlds.com/v1/upload on production). */
1958
+ upload_url: string;
1959
+ /** `u/<world_id>/`: every upload with this ticket lands under it. */
1960
+ prefix: string;
1961
+ /** The most this ticket accepts: the per-image limit, or what the account has left, whichever is smaller. */
1962
+ max_bytes: number;
1963
+ /** Unix seconds after which the edge refuses the ticket. */
1964
+ exp: number;
1965
+ /** Always 1: one ticket, one upload. */
1966
+ uses: number;
1967
+ /** Who answers for the upload; the edge copies it onto the stored object. */
1968
+ issued_to: {
1969
+ account: string | null;
1970
+ membership: string | null;
1971
+ };
1972
+ }
1973
+ /** The edge's 201 for one upload. Put `url` in an element's `image_url`. */
1974
+ interface OwUploadedImage {
1975
+ /** The permanent public URL (https://media.onlyworlds.com/<key>). */
1976
+ url: string;
1977
+ /** The object key, under the ticket's `prefix`. */
1978
+ key: string;
1979
+ bytes: number;
1980
+ /** The MIME type the edge read from the bytes (never from a header or a file name). */
1981
+ type: string;
1982
+ etag: string;
1983
+ [k: string]: unknown;
1984
+ }
1950
1985
  interface ListParams {
1951
1986
  limit?: number;
1952
1987
  cursor?: string;
@@ -1955,35 +1990,36 @@ interface ListParams {
1955
1990
  /** Sparse include-set of field names. */
1956
1991
  fields?: string[];
1957
1992
  /**
1958
- * Accepted: `name__icontains`, `supertype`, `subtype`. Any other key 422s
1959
- * server-side, which names the typo -- the client passes keys through
1960
- * unchecked and lets the platform say so. (`__in`, `__gte`, `__lte` and
1993
+ * Accepted: `name` (exact), `name__icontains`, `supertype`, `subtype`, and
1994
+ * `characters=<id>` (contains) on every type with a `characters` link (at schema
1995
+ * 0.30.1: collective, construct, event, narrative, relation, title; keel D76d). Any other key 422s
1996
+ * server-side with the list of filters in the message -- the client passes keys
1997
+ * through unchecked and lets the platform say so. (`__in`, `__gte`, `__lte` and
1961
1998
  * `__isnull` are designed in keel's spec but not built; `ordering` 422s too.)
1962
1999
  */
1963
2000
  filter?: Record<string, string | number | boolean>;
1964
2001
  }
1965
2002
  interface OwClientConfig {
1966
- /** 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. */
1967
2004
  apiKey: string;
1968
2005
  /**
1969
- * Optional. Required for writes when the world has a PIN, and for legacy-key
1970
- * reads of private worlds. Prefixed keys read PIN-less. String, not number --
1971
- * '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.
1972
2008
  */
1973
2009
  apiPin?: string;
1974
- /** Default: https://www.onlyworlds.com/api/v2 */
2010
+ /** The API's base URL, default https://www.onlyworlds.com/api/v2. */
1975
2011
  baseUrl?: string;
1976
2012
  /**
1977
- * Page size for element lists. Default 100 (server default; max 1000).
1978
- * 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.
1979
2015
  */
1980
2016
  pageSize?: number;
1981
2017
  /**
1982
- * Page size for /changes pulls. Default 100. Live precedents: Obsidian 100,
1983
- * 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.
1984
2020
  */
1985
2021
  changesPageSize?: number;
1986
- /** 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). */
1987
2023
  fetch?: typeof globalThis.fetch;
1988
2024
  }
1989
2025
 
@@ -1997,7 +2033,12 @@ interface OwClientConfig {
1997
2033
  * envelope also carries `type` and `param` (fixtures P4a/P2c); both are
1998
2034
  * surfaced on the thrown error.
1999
2035
  */
2000
- /** Auth codes are distinguishable by design; client recovery UX differs per code. */
2036
+ /**
2037
+ * Auth codes are distinguishable by design; client recovery UX differs per code.
2038
+ * `world_gone` is RESERVED: keel's spec promises it but the server never emits it (a deleted
2039
+ * world's keys go with it, so the key reads as unknown: 401 `invalid_credentials`). Kept so
2040
+ * the client is ready if keel ever remembers deleted worlds' key hashes.
2041
+ */
2001
2042
  type OwAuthErrorCode = 'invalid_credentials' | 'key_revoked' | 'world_gone';
2002
2043
  declare class OwApiError extends Error {
2003
2044
  readonly status: number;
@@ -2037,6 +2078,13 @@ declare class OwApiError extends Error {
2037
2078
  * per-item slot `{status: 409, error: {code: 'id_conflict'}}` — check the slot.
2038
2079
  */
2039
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;
2040
2088
  /** keel's admission control turned the request away (503 `server_busy`); see retryAfter. */
2041
2089
  get isBusy(): boolean;
2042
2090
  /**
@@ -2047,9 +2095,10 @@ declare class OwApiError extends Error {
2047
2095
  */
2048
2096
  get isNotAuthor(): boolean;
2049
2097
  /**
2050
- * A contributor tried a world-level change: `patchWorld`, or publishing a show page
2051
- * (403 `owner_only`, keel D72 membership phase 2). Retrying will not help; only the
2052
- * owner may. Owners and co-builders never see it.
2098
+ * A member key tried what only the owner may do: `patchWorld`, even with a co-builder's
2099
+ * key (403 `owner_only`, keel D72; sharing's publish, republish and revoke answer
2100
+ * contributors and guests with the same code). Retrying will not help. The owner's own
2101
+ * key never sees it.
2053
2102
  */
2054
2103
  get isOwnerOnly(): boolean;
2055
2104
  }
@@ -2132,11 +2181,11 @@ declare class OwV2Client {
2132
2181
  /** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
2133
2182
  get(type: ElementType | string, id: string, opts?: Pick<ListParams, 'expand' | 'fields'>): Promise<OwElement>;
2134
2183
  /**
2135
- * POST /{type}/ -- create. Mints an RFC 9562 UUIDv7 for element.id when the
2136
- * caller omits one (design ruling D29d) so a retry carrying the same
2137
- * 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.
2138
2187
  * `name` is the one required field, and required means the key is present: `''` and
2139
- * `null` are accepted and stored as `''` (nameless Markers exist; keel ruling 2026-09-28).
2188
+ * `null` are accepted and stored as `''` (nameless Markers exist).
2140
2189
  */
2141
2190
  create(type: ElementType | string, element: OwElement | Record<string, unknown>, opts?: {
2142
2191
  idempotencyKey?: string;
@@ -2180,10 +2229,12 @@ declare class OwV2Client {
2180
2229
  }): Promise<OwBulkResponse>;
2181
2230
  /**
2182
2231
  * GET /changes -- one page of the world's ordered change feed.
2183
- * 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).
2184
2234
  * Zero/absent cursor = full export (byte-aligned with the Folder Format).
2185
- * Rewind rule: if your persisted position is ahead of page.head, the server
2186
- * 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.
2187
2238
  * Citizenship: heaviest route on the platform; default page size is polite.
2188
2239
  */
2189
2240
  changes(opts?: {
@@ -2199,6 +2250,36 @@ declare class OwV2Client {
2199
2250
  cursor: string;
2200
2251
  head: number;
2201
2252
  }>;
2253
+ /**
2254
+ * POST /media/ticket -- permission to upload ONE image into this world (a write
2255
+ * key and its PIN; no body). Short-lived (`exp`), single-use, counted toward the
2256
+ * world's daily limit. Use it directly to upload yourself (for progress, say):
2257
+ * POST the bytes to `upload_url` with `Authorization: Bearer <ticket>`. Otherwise
2258
+ * call uploadImage(), which does both steps. Refusals: 403 `storage_full` (the
2259
+ * account's image storage is used up), 429 `quota_exceeded` (the world's daily
2260
+ * limit; Retry-After) as well as the wrong-PIN 429 `rate_limited`, 503
2261
+ * `media_unavailable`.
2262
+ */
2263
+ createMediaTicket(): Promise<OwMediaTicket>;
2264
+ /**
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
2267
+ * createMediaTicket() to use it instead of fetching one. Returns the edge's 201;
2268
+ * set `url` as an element's `image_url` yourself (`patch(type, id, { image_url })`).
2269
+ *
2270
+ * webp, png, jpeg or avif, read from the bytes (never SVG or gif). `key` names the
2271
+ * object (it must start with the ticket's `prefix`; without it the edge picks one).
2272
+ * An image larger than the ticket's `max_bytes` throws 413 `too_large` before any
2273
+ * byte is sent. The edge's refusals arrive as OwApiError with its code: 401
2274
+ * `ticket_expired` / `ticket_used` / `ticket_invalid`, 400 `bad_key`, 409 `exists`,
2275
+ * 413 `too_large`, 415 `unsupported_type`, 502 `write_failed` (nothing stored; the
2276
+ * ticket is still unspent). A ticket is spent once the upload succeeds: a retry
2277
+ * needs a new one.
2278
+ */
2279
+ uploadImage(image: Blob | ArrayBuffer | ArrayBufferView, opts?: {
2280
+ key?: string;
2281
+ ticket?: OwMediaTicket;
2282
+ }): Promise<OwUploadedImage>;
2202
2283
  /**
2203
2284
  * Raw authenticated request against this client's baseUrl. Public since 4.0
2204
2285
  * so auxiliary resources can ride the same transport — it structurally
@@ -2573,4 +2654,4 @@ declare class TokenResource {
2573
2654
  getEncryptionInfo(): Promise<EncryptionInfo>;
2574
2655
  }
2575
2656
 
2576
- 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, OwNetworkError, type OwPage, 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 };
2657
+ 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 };
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";
@@ -49,9 +58,10 @@ var OwApiError = class extends Error {
49
58
  return this.status === 403 && this.code === "not_author";
50
59
  }
51
60
  /**
52
- * A contributor tried a world-level change: `patchWorld`, or publishing a show page
53
- * (403 `owner_only`, keel D72 membership phase 2). Retrying will not help; only the
54
- * owner may. Owners and co-builders never see it.
61
+ * A member key tried what only the owner may do: `patchWorld`, even with a co-builder's
62
+ * key (403 `owner_only`, keel D72; sharing's publish, republish and revoke answer
63
+ * contributors and guests with the same code). Retrying will not help. The owner's own
64
+ * key never sees it.
55
65
  */
56
66
  get isOwnerOnly() {
57
67
  return this.status === 403 && this.code === "owner_only";
@@ -187,11 +197,11 @@ var OwV2Client = class {
187
197
  return this.request("GET", `/${type}/${id}/`, { query });
188
198
  }
189
199
  /**
190
- * POST /{type}/ -- create. Mints an RFC 9562 UUIDv7 for element.id when the
191
- * caller omits one (design ruling D29d) so a retry carrying the same
192
- * 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.
193
203
  * `name` is the one required field, and required means the key is present: `''` and
194
- * `null` are accepted and stored as `''` (nameless Markers exist; keel ruling 2026-09-28).
204
+ * `null` are accepted and stored as `''` (nameless Markers exist).
195
205
  */
196
206
  async create(type, element, opts = {}) {
197
207
  const body = sanitizePayload(element);
@@ -259,10 +269,12 @@ var OwV2Client = class {
259
269
  // -- Changes feed --------------------------------------------------------
260
270
  /**
261
271
  * GET /changes -- one page of the world's ordered change feed.
262
- * 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).
263
274
  * Zero/absent cursor = full export (byte-aligned with the Folder Format).
264
- * Rewind rule: if your persisted position is ahead of page.head, the server
265
- * 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.
266
278
  * Citizenship: heaviest route on the platform; default page size is polite.
267
279
  */
268
280
  async changes(opts = {}) {
@@ -284,6 +296,58 @@ var OwV2Client = class {
284
296
  } while (page.has_more);
285
297
  return { cursor: page.cursor, head: page.head };
286
298
  }
299
+ // -- Images ---------------------------------------------------------------
300
+ /**
301
+ * POST /media/ticket -- permission to upload ONE image into this world (a write
302
+ * key and its PIN; no body). Short-lived (`exp`), single-use, counted toward the
303
+ * world's daily limit. Use it directly to upload yourself (for progress, say):
304
+ * POST the bytes to `upload_url` with `Authorization: Bearer <ticket>`. Otherwise
305
+ * call uploadImage(), which does both steps. Refusals: 403 `storage_full` (the
306
+ * account's image storage is used up), 429 `quota_exceeded` (the world's daily
307
+ * limit; Retry-After) as well as the wrong-PIN 429 `rate_limited`, 503
308
+ * `media_unavailable`.
309
+ */
310
+ async createMediaTicket() {
311
+ return this.request("POST", "/media/ticket");
312
+ }
313
+ /**
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
316
+ * createMediaTicket() to use it instead of fetching one. Returns the edge's 201;
317
+ * set `url` as an element's `image_url` yourself (`patch(type, id, { image_url })`).
318
+ *
319
+ * webp, png, jpeg or avif, read from the bytes (never SVG or gif). `key` names the
320
+ * object (it must start with the ticket's `prefix`; without it the edge picks one).
321
+ * An image larger than the ticket's `max_bytes` throws 413 `too_large` before any
322
+ * byte is sent. The edge's refusals arrive as OwApiError with its code: 401
323
+ * `ticket_expired` / `ticket_used` / `ticket_invalid`, 400 `bad_key`, 409 `exists`,
324
+ * 413 `too_large`, 415 `unsupported_type`, 502 `write_failed` (nothing stored; the
325
+ * ticket is still unspent). A ticket is spent once the upload succeeds: a retry
326
+ * needs a new one.
327
+ */
328
+ async uploadImage(image, opts = {}) {
329
+ const ticket = opts.ticket ?? await this.createMediaTicket();
330
+ const size = typeof Blob !== "undefined" && image instanceof Blob ? image.size : image.byteLength;
331
+ if (size > ticket.max_bytes) {
332
+ throw new OwApiError(
333
+ 413,
334
+ "too_large",
335
+ `Image is ${size} bytes; this ticket accepts at most ${ticket.max_bytes}. Nothing was sent.`,
336
+ null,
337
+ { error: "too_large", max_bytes: ticket.max_bytes, got: size }
338
+ );
339
+ }
340
+ const headers = { Authorization: `Bearer ${ticket.ticket}` };
341
+ if (opts.key) headers["X-Key"] = opts.key;
342
+ let res;
343
+ try {
344
+ res = await this.fetchImpl(ticket.upload_url, { method: "POST", headers, body: image });
345
+ } catch (cause) {
346
+ throw new OwNetworkError(`OnlyWorlds image upload failed: POST ${ticket.upload_url}`, cause);
347
+ }
348
+ if (!res.ok) throw await errorFromResponse(res);
349
+ return await res.json();
350
+ }
287
351
  // -- Core request machinery ----------------------------------------------
288
352
  /**
289
353
  * Raw authenticated request against this client's baseUrl. Public since 4.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {