@onlyworlds/sdk 4.5.0 → 4.6.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
@@ -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,42 @@ 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.6.0] — 2026-10-06 (staged, not published)
9
+
10
+ Includes 4.5.1's documentation fixes: 4.5.1 was staged and never published.
11
+
12
+ ### 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
15
+ (`POST /media/ticket`), then the bytes straight to the edge at the ticket's `upload_url`
16
+ (keel never sees them). Only the ticket goes to the edge, never the API key or PIN. An image
17
+ larger than the ticket's `max_bytes` throws 413 `too_large` before anything is sent; the edge's
18
+ refusals (`ticket_used`, `ticket_expired`, `bad_key`, `exists`, `unsupported_type`, ...) arrive
19
+ as `OwApiError` with their code. Set the returned `url` as an element's `image_url` yourself.
20
+ 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).
22
+ - **`createMediaTicket()`**: the first step alone, for callers that upload themselves (progress
23
+ bars, say). Types `OwMediaTicket` and `OwUploadedImage`.
24
+
25
+ ### Documented
26
+ - `ListParams.filter` lists what keel accepts now: exact `name`, and `characters=<id>` on every
27
+ type with a `characters` link (keel D76d).
28
+
29
+ ## [4.5.1] — 2026-10-05 (never published; shipped in 4.6.0)
30
+
31
+ Docs only: no code path changes.
32
+
33
+ ### Documented
34
+ - **`isOwnerOnly`'s JSDoc in 4.5.0 was wrong about who sees `owner_only`.** It said owners and
35
+ 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,
37
+ 2026-10-04).
38
+ - **`world_gone` is reserved.** `OwAuthErrorCode` and `isAuthError` still carry it, but keel never
39
+ 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
41
+ built file for the codes it keys on.
42
+
43
+ ## [4.5.0] — 2026-10-04
9
44
 
10
45
  What the staging write probes found on the wire (Python package, 2026-10-04): one new getter, and
11
46
  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, 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>`.
105
+
93
106
  ## Sync
94
107
 
95
108
  ```typescript
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,9 +1990,11 @@ 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>;
@@ -1997,7 +2034,12 @@ interface OwClientConfig {
1997
2034
  * envelope also carries `type` and `param` (fixtures P4a/P2c); both are
1998
2035
  * surfaced on the thrown error.
1999
2036
  */
2000
- /** Auth codes are distinguishable by design; client recovery UX differs per code. */
2037
+ /**
2038
+ * Auth codes are distinguishable by design; client recovery UX differs per code.
2039
+ * `world_gone` is RESERVED: keel's spec promises it but the server never emits it (a deleted
2040
+ * world's keys go with it, so the key reads as unknown: 401 `invalid_credentials`). Kept so
2041
+ * the client is ready if keel ever remembers deleted worlds' key hashes.
2042
+ */
2001
2043
  type OwAuthErrorCode = 'invalid_credentials' | 'key_revoked' | 'world_gone';
2002
2044
  declare class OwApiError extends Error {
2003
2045
  readonly status: number;
@@ -2047,9 +2089,10 @@ declare class OwApiError extends Error {
2047
2089
  */
2048
2090
  get isNotAuthor(): boolean;
2049
2091
  /**
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.
2092
+ * A member key tried what only the owner may do: `patchWorld`, even with a co-builder's
2093
+ * key (403 `owner_only`, keel D72; sharing's publish, republish and revoke answer
2094
+ * contributors and guests with the same code). Retrying will not help. The owner's own
2095
+ * key never sees it.
2053
2096
  */
2054
2097
  get isOwnerOnly(): boolean;
2055
2098
  }
@@ -2199,6 +2242,36 @@ declare class OwV2Client {
2199
2242
  cursor: string;
2200
2243
  head: number;
2201
2244
  }>;
2245
+ /**
2246
+ * POST /media/ticket -- permission to upload ONE image into this world (a write
2247
+ * key and its PIN; no body). Short-lived (`exp`), single-use, counted toward the
2248
+ * world's daily limit. Use it directly to upload yourself (for progress, say):
2249
+ * POST the bytes to `upload_url` with `Authorization: Bearer <ticket>`. Otherwise
2250
+ * call uploadImage(), which does both steps. Refusals: 403 `storage_full` (the
2251
+ * account's image storage is used up), 429 `quota_exceeded` (the world's daily
2252
+ * limit; Retry-After) as well as the wrong-PIN 429 `rate_limited`, 503
2253
+ * `media_unavailable`.
2254
+ */
2255
+ createMediaTicket(): Promise<OwMediaTicket>;
2256
+ /**
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
2259
+ * createMediaTicket() to use it instead of fetching one. Returns the edge's 201;
2260
+ * set `url` as an element's `image_url` yourself (`patch(type, id, { image_url })`).
2261
+ *
2262
+ * webp, png, jpeg, avif or gif, read from the bytes (never SVG). `key` names the
2263
+ * object (it must start with the ticket's `prefix`; without it the edge picks one).
2264
+ * An image larger than the ticket's `max_bytes` throws 413 `too_large` before any
2265
+ * byte is sent. The edge's refusals arrive as OwApiError with its code: 401
2266
+ * `ticket_expired` / `ticket_used` / `ticket_invalid`, 400 `bad_key`, 409 `exists`,
2267
+ * 413 `too_large`, 415 `unsupported_type`, 502 `write_failed` (nothing stored; the
2268
+ * ticket is still unspent). A ticket is spent once the upload succeeds: a retry
2269
+ * needs a new one.
2270
+ */
2271
+ uploadImage(image: Blob | ArrayBuffer | ArrayBufferView, opts?: {
2272
+ key?: string;
2273
+ ticket?: OwMediaTicket;
2274
+ }): Promise<OwUploadedImage>;
2202
2275
  /**
2203
2276
  * Raw authenticated request against this client's baseUrl. Public since 4.0
2204
2277
  * so auxiliary resources can ride the same transport — it structurally
@@ -2573,4 +2646,4 @@ declare class TokenResource {
2573
2646
  getEncryptionInfo(): Promise<EncryptionInfo>;
2574
2647
  }
2575
2648
 
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 };
2649
+ 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
@@ -49,9 +49,10 @@ var OwApiError = class extends Error {
49
49
  return this.status === 403 && this.code === "not_author";
50
50
  }
51
51
  /**
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.
52
+ * A member key tried what only the owner may do: `patchWorld`, even with a co-builder's
53
+ * key (403 `owner_only`, keel D72; sharing's publish, republish and revoke answer
54
+ * contributors and guests with the same code). Retrying will not help. The owner's own
55
+ * key never sees it.
55
56
  */
56
57
  get isOwnerOnly() {
57
58
  return this.status === 403 && this.code === "owner_only";
@@ -284,6 +285,58 @@ var OwV2Client = class {
284
285
  } while (page.has_more);
285
286
  return { cursor: page.cursor, head: page.head };
286
287
  }
288
+ // -- Images ---------------------------------------------------------------
289
+ /**
290
+ * POST /media/ticket -- permission to upload ONE image into this world (a write
291
+ * key and its PIN; no body). Short-lived (`exp`), single-use, counted toward the
292
+ * world's daily limit. Use it directly to upload yourself (for progress, say):
293
+ * POST the bytes to `upload_url` with `Authorization: Bearer <ticket>`. Otherwise
294
+ * call uploadImage(), which does both steps. Refusals: 403 `storage_full` (the
295
+ * account's image storage is used up), 429 `quota_exceeded` (the world's daily
296
+ * limit; Retry-After) as well as the wrong-PIN 429 `rate_limited`, 503
297
+ * `media_unavailable`.
298
+ */
299
+ async createMediaTicket() {
300
+ return this.request("POST", "/media/ticket");
301
+ }
302
+ /**
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
305
+ * createMediaTicket() to use it instead of fetching one. Returns the edge's 201;
306
+ * set `url` as an element's `image_url` yourself (`patch(type, id, { image_url })`).
307
+ *
308
+ * webp, png, jpeg, avif or gif, read from the bytes (never SVG). `key` names the
309
+ * object (it must start with the ticket's `prefix`; without it the edge picks one).
310
+ * An image larger than the ticket's `max_bytes` throws 413 `too_large` before any
311
+ * byte is sent. The edge's refusals arrive as OwApiError with its code: 401
312
+ * `ticket_expired` / `ticket_used` / `ticket_invalid`, 400 `bad_key`, 409 `exists`,
313
+ * 413 `too_large`, 415 `unsupported_type`, 502 `write_failed` (nothing stored; the
314
+ * ticket is still unspent). A ticket is spent once the upload succeeds: a retry
315
+ * needs a new one.
316
+ */
317
+ async uploadImage(image, opts = {}) {
318
+ const ticket = opts.ticket ?? await this.createMediaTicket();
319
+ const size = typeof Blob !== "undefined" && image instanceof Blob ? image.size : image.byteLength;
320
+ if (size > ticket.max_bytes) {
321
+ throw new OwApiError(
322
+ 413,
323
+ "too_large",
324
+ `Image is ${size} bytes; this ticket accepts at most ${ticket.max_bytes}. Nothing was sent.`,
325
+ null,
326
+ { error: "too_large", max_bytes: ticket.max_bytes, got: size }
327
+ );
328
+ }
329
+ const headers = { Authorization: `Bearer ${ticket.ticket}` };
330
+ if (opts.key) headers["X-Key"] = opts.key;
331
+ let res;
332
+ try {
333
+ res = await this.fetchImpl(ticket.upload_url, { method: "POST", headers, body: image });
334
+ } catch (cause) {
335
+ throw new OwNetworkError(`OnlyWorlds image upload failed: POST ${ticket.upload_url}`, cause);
336
+ }
337
+ if (!res.ok) throw await errorFromResponse(res);
338
+ return await res.json();
339
+ }
287
340
  // -- Core request machinery ----------------------------------------------
288
341
  /**
289
342
  * 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.6.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {