@onlyworlds/sdk 4.55.0 → 4.57.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,6 +1,6 @@
1
1
  # For AI agents using @onlyworlds/sdk
2
2
 
3
- **Current as of**: SDK **4.x** · schema-dist **v0.33.0-dist.45** (canonical 00.33.00).
3
+ **Current as of**: SDK **4.x** · schema-dist **v0.33.0-dist.46** (canonical 00.33.00).
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
 
@@ -30,8 +30,10 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
30
30
  - Offline or stale edits: pass `{ ifMatch: el.change_seq }` to `patch`/`editLinks` (since 4.9.0).
31
31
  If the element moved, nothing is written and `OwPreconditionFailedError` (412) carries
32
32
  `current`; the server never merges, the client decides.
33
- - Polling: `changesHead()` (since 4.15.0) gives `{head, cursor}` without a page; walk only
34
- when `head` moved past the head you last applied.
33
+ - Polling: `changesHead()` (since 4.15.0) gives `{head, cursor, epoch, unchanged}` without a
34
+ page; walk only when `head` or `epoch` moved past what you last applied. Since 4.57.0 it
35
+ sends the last ETag as `If-None-Match`: a 304 (refunded by keel) comes back as the held
36
+ values with `unchanged: true`. The client never sleeps or picks an interval.
35
37
  - World-meta changes do NOT appear in `/changes` — poll `GET /world` separately.
36
38
  - Extension fields: `x_<toolname>_*` is the sanctioned namespace for tool-specific state;
37
39
  unknown unprefixed fields 422. Extensions are capped at **64 KB per element** (422,
@@ -109,6 +111,11 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
109
111
  `mcp`, `declared`, `api`, `v1`, `sync`, `server`), and a `/changes` delete carries
110
112
  `deleted_by` and `deleted_via`; `isAgentDoor(door)` is true for the first three. An agent
111
113
  seat's key is recorded as an agent without the option. Never set it in a tool people use.
114
+ - **Name your tool** (since 4.56.0): `new OwV2Client({ ..., client: 'atlas' })` sends
115
+ `X-OW-Client: atlas` on every element write (never on a read); left out, it sends `sdk`. The name
116
+ is lower case letters, digits and hyphens, 1 to 32 characters, else a TypeError at construction.
117
+ keel stores it as `changed_client` on the element and `deleted_client` on a `/changes` delete:
118
+ the client's claim, never proof, and null when none was sent.
112
119
  - A guest or player reads what its own Character's sheet links to as a **stub**, marked
113
120
  `known_of: true` (`isKnownOf(el)`): only `id`, `type`, `name`, `description`, `supertype`,
114
121
  `subtype`, `image_url`, the timestamps and the fields its type lists in `PUBLIC_FIELDS` are
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.57.0] - 2026-10-22
9
+
10
+ ### Added
11
+
12
+ - **The conditional head poll** (keel #518, live 2026-10-22; Orrery's `revision-poll.md`, decision 3): `changesHead()` keeps the last head read's `ETag` per client and sends it back as `If-None-Match` on the next one, exactly as received (keel's 200 sends a weak `W/"821:-"`; nothing is quoted, parsed or compared). keel's tag is `"<head>:<epoch>"`, so a view change moves it as a new change does. When nothing moved, keel answers 304 Not Modified with no body, and `changesHead()` returns the `head`, `cursor` and `epoch` of the last 200 with `unchanged: true`; every 200 says `unchanged: false`. keel refunds a 304 against the request budget, so a poll that mostly finds nothing costs little. A 409 `resync_required` from `changesHead()`, `changes()` or `changesAll()` clears the held tag, so the next head read is a full 200 with `epoch_reason`. A 304 while no tag is held is an `OwApiError` with status 304, never a made-up head. The client still never polls or sleeps on its own: the interval is the caller's.
13
+ - **The public page on `OwWorldMeta`** (keel #471, PR 173, live 2026-10-22), both read-only: `slug` (`string | null`), the page's address, onlyworlds.com/w/<slug>, null while the page is off, and also a keyless `?world=` value; `public_page` (the new `OwWorldPublicPage`), the page as its owner set it: `on`, `palette` (midnight, daylight, parchment, brass, dusk, or null for the house's), `hero` (one plain line), `sections` (the types shown in order, empty for all), `pin` (an element id or null) and `index` (always false for now), every key present with the page off. Both are set by the owner on the world page; `patchWorld()` strips them, so a body read from `getWorld()` stays writable.
14
+ - **`request()` gains two options**: `ifNoneMatch` (a tag sent as `If-None-Match` as given) and `onEtag(etag, status)` (called with each response's `ETag` and status before the body is read; with it given, a 304 returns null instead of throwing). Every other path is unchanged.
15
+
16
+ ### Changed
17
+
18
+ - **`changesHead()`'s return type gains `unchanged: boolean`**, beside `head`, `cursor`, `epoch` and the optional `epoch_reason`.
19
+
20
+ ## [4.56.0] - 2026-10-22
21
+
22
+ ### Added
23
+
24
+ - **The client's name** (keel #520, live 2026-10-22; Orrery's `client-name.md`, decision 2): a constructor option `client`, default `'sdk'`, sent as `X-OW-Client` on every element write (`create`, `upsert`, `patch`, `delete`, `editLinks`, `bulk`, `adopt`) and on no read. keel stores it on the element as `changed_client`, so a reader can tell which tool made the last change (`new OwV2Client({ ..., client: 'atlas' })`). The rule is keel's, exact here: lower case letters, digits and hyphens, 1 to 32 characters (`[a-z0-9-]{1,32}`). Anything else, a capital, a space, an empty string, 33 characters or a value that is not a string, is a TypeError at construction naming the option and the rule, and nothing is trimmed; keel's own fence for a stranger's header is to store null, never a 422. The name is the client's claim about itself, never proof; it changes no door and no permission, and `agent` still decides `X-OW-Agent`.
25
+ - **`changed_client`** on `OwElementBase` (`string | null`, server-managed, read-only, beside `changed_by` and `changed_via`): the name the last write gave, null when it gave none or one outside the rule, for keel's own writes, and for changes from before names were recorded. Sent back in a write it is ignored. **`deleted_client`** on a `/changes` delete (`OwChangeDelete`, beside `deleted_by` and `deleted_via`), the same for the delete; null on a `hidden` row.
26
+
27
+ ### Changed
28
+
29
+ - **The constructor refuses an unknown key by name**, as every options object has since 4.52.0: `new OwV2Client({ clinet: 'atlas' })` is a `TypeError` naming the key and the eight the constructor knows, where it fell back to the defaults silently before. Every known caller (Atlas, Lantern, the toolkit) passes known keys only.
30
+ - **Schema repinned to schema-dist v0.33.0-dist.46** (same canonical 00.33.00). The dist's example export (`example/world-export.json`) gains `changed_client: null` on its bodies, `walk/lint_folder.py`'s server-key set names `changed_client`, and the README's sentence on server keys names it; `VERSION` and `MANIFEST.json` carry the new serial; no schema file moved. Generated output moves its provenance lines and gains `changed_client`.
31
+
8
32
  ## [4.55.0] - 2026-10-22
9
33
 
10
34
  ### Added
package/README.md CHANGED
@@ -180,7 +180,7 @@ for await (const change of client.changesAll(cursor)) {
180
180
  }
181
181
  ```
182
182
 
183
- To poll for news cheaply, `client.changesHead()` returns `{ head, cursor }` without reading any element: walk `changesAll` only when `head` moved past the head you last applied.
183
+ To poll for news cheaply, `client.changesHead()` returns `{ head, cursor, epoch, unchanged }` without reading any element: walk `changesAll` only when `head` or `epoch` moved past what you last applied. Since 4.57.0 the client sends the last answer's ETag back, so when nothing moved keel answers 304 (refunded against your request budget) and `unchanged` is true. Sleep on it; the interval is yours.
184
184
 
185
185
  Edits to the world itself (its name, calendar, `public_read`) don't appear in the change feed. Read `client.getWorld()` and compare `updated_at`.
186
186
 
package/SCHEMA.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OnlyWorlds Schema Reference
2
2
 
3
- **Source**: https://github.com/OnlyWorlds/schema-dist @ **v0.33.0-dist.45** — canonical schema **00.33.00**, published 2026-10-22.
3
+ **Source**: https://github.com/OnlyWorlds/schema-dist @ **v0.33.0-dist.46** — canonical schema **00.33.00**, published 2026-10-22.
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.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Every element carries these. The extension index signature admits namespaced
2
2
  * pass-through fields (atlas_* / shadow_* / x_*) returned verbatim by the server.
3
3
  * Derived from base_properties.yaml: `World` is dropped (the API rejects it in
4
- * bodies -- the key determines the world) and the seven server-managed fields are
4
+ * bodies -- the key determines the world) and the eight server-managed fields are
5
5
  * added, since they ride every wire body and appear in no element YAML; `known_of`
6
6
  * rides only a stub. */
7
7
  interface OwElementBase {
@@ -31,6 +31,8 @@ interface OwElementBase {
31
31
  changed_by?: string | null;
32
32
  /** The door of the last write (server-managed, read-only): `seat` (an agent seat), `mcp`, `declared` (a client that sent X-OW-Agent), then `api`, `v1`, `sync`, `server` (keel's own writes) or `site` (the owner signed in on onlyworlds.com; keel PR 160). The first three are agent-made. Null for anything last changed before writes recorded it. */
33
33
  changed_via?: 'seat' | 'mcp' | 'declared' | 'api' | 'v1' | 'sync' | 'server' | 'site' | null;
34
+ /** The name the client gave in the X-OW-Client header on the last write (server-managed, read-only): lower case letters, digits and hyphens, 1 to 32 characters, such as `sdk` or `atlas`. It is the client's claim about itself, never proof, and says nothing about whether an agent drove the write (that is `changed_via`). Null when the last write sent no name or one outside the rule, for keel's own writes, and for anything last changed before names were recorded. Sent in a write it is ignored. This SDK sends its `client` option, `sdk` by default. */
35
+ changed_client?: string | null;
34
36
  /** Present, and true, only when the world's owner or a co-builder hid this element and this reader still sees it (server-managed, read-only): the owner, a co-builder, a key the owner minted, or the member who made it; to every other reader the element does not exist. Absent otherwise, whatever `fields` asked; on the read, the list, the change feed's upserts and MCP, never in the download, world sync or v1. Sent in a write it is ignored. */
35
37
  hidden?: true;
36
38
  /** Present, and true, only on a stub (server-managed, read-only): an element a guest or player reaches only through its own Character's sheet. A stub keeps id, type, name, description, supertype, subtype, image_url, created_at, updated_at and change_seq, and the fields its type lists in PUBLIC_FIELDS with their values; every other field reads unset ("", null, []), created_by/changed_by/changed_via null, no extension fields, and its files are 404. Its empties are not values and not a deletion: never write a stub back. Absent: the element is whole. See isKnownOf(). */
@@ -1945,11 +1947,38 @@ interface OwWorldMeta {
1945
1947
  agent_narrator_rule?: boolean;
1946
1948
  /** The owner's "this Character is me": a Character id in this world, or null (null too, to a reader it is hidden from: keel #421). */
1947
1949
  owner_character?: string | null;
1950
+ /**
1951
+ * The world's public page address, onlyworlds.com/w/<slug> (keel #471): null when the world has
1952
+ * no page (a slug exists only while the page is on). Also a keyless `?world=` value. Set by the
1953
+ * owner on the world page; read-only on the wire, and patchWorld strips it.
1954
+ */
1955
+ slug?: string | null;
1956
+ /**
1957
+ * The world's public page as its owner set it (keel #471): `on`, and the levers. Every key is
1958
+ * present with the page off. Set by the owner on the world page; read-only on the wire, and
1959
+ * patchWorld strips it.
1960
+ */
1961
+ public_page?: OwWorldPublicPage;
1948
1962
  created_at?: string | null;
1949
1963
  /** Moves when the world's own fields change, not on element writes. */
1950
1964
  updated_at?: string | null;
1951
1965
  [field: string]: unknown;
1952
1966
  }
1967
+ /** OwWorldMeta.public_page (keel #471), as keel's `World` component types it: all six keys, always. */
1968
+ interface OwWorldPublicPage {
1969
+ /** Whether the page is on. */
1970
+ on: boolean;
1971
+ /** The page's palette, or null for the house's. */
1972
+ palette: 'midnight' | 'daylight' | 'parchment' | 'brass' | 'dusk' | null;
1973
+ /** One plain line (at most 120 characters). */
1974
+ hero: string;
1975
+ /** The element types shown, in their order; empty for all, in the house order. */
1976
+ sections: string[];
1977
+ /** One element's id, or null. */
1978
+ pin: string | null;
1979
+ /** Always false for now: search engines stay off. */
1980
+ index: boolean;
1981
+ }
1953
1982
  /** List envelope: cursor-paginated. */
1954
1983
  interface OwPage<T = OwElement> {
1955
1984
  data: T[];
@@ -1981,6 +2010,12 @@ interface OwChangeDelete {
1981
2010
  deleted_by?: string | null;
1982
2011
  /** The door the delete came through (see {@link OwWriteDoor}); null for deletes before writes recorded it. */
1983
2012
  deleted_via?: OwWriteDoor | null;
2013
+ /**
2014
+ * The name the deleting client gave in its `X-OW-Client` header (keel #520), as an element's
2015
+ * `changed_client`: the client's claim, never proof. Null when none was recorded, and on a
2016
+ * `hidden` row (a hide carries no client).
2017
+ */
2018
+ deleted_client?: string | null;
1984
2019
  /**
1985
2020
  * Why the element left this reader's view (keel #421): `deleted` (it is gone; a GET answers 410),
1986
2021
  * `hidden` (the world's owner or a co-builder hid it from this reader; it still exists, a GET answers
@@ -2812,6 +2847,18 @@ interface OwClientConfig {
2812
2847
  * as an agent without it.
2813
2848
  */
2814
2849
  agent?: boolean;
2850
+ /**
2851
+ * This client's own name, sent as `X-OW-Client` on every element write (create, upsert, patch,
2852
+ * delete, editLinks, bulk, adopt) and on no read (keel #520). keel stores it on the element as
2853
+ * `changed_client`, and on a /changes delete as `deleted_client`, so a reader can tell which tool
2854
+ * made a change: `'atlas'`, `'lantern'`, your app's name. Default `'sdk'`.
2855
+ *
2856
+ * The rule: lower case letters, digits and hyphens, 1 to 32 characters (`[a-z0-9-]{1,32}`), no
2857
+ * version (`'atlas'`, not `'atlas-1.76'`). Anything else is a TypeError when the client is
2858
+ * constructed, and nothing is trimmed. It is the client's claim about itself, not proof, and it
2859
+ * changes no permission and no door: whether an agent made a write is `agent`'s business.
2860
+ */
2861
+ client?: string;
2815
2862
  }
2816
2863
 
2817
2864
  /**
@@ -3158,6 +3205,10 @@ declare class OwV2Client {
3158
3205
  private readonly apiPin;
3159
3206
  private readonly fetchImpl;
3160
3207
  private readonly agent;
3208
+ /** The name sent as X-OW-Client on element writes (keel #520), checked at construction. */
3209
+ private readonly clientName;
3210
+ /** The conditional head poll's state (keel #518), per instance. */
3211
+ private readonly headPoll;
3161
3212
  constructor(config?: OwClientConfig);
3162
3213
  /** GET /health -- unauthenticated liveness pulse. */
3163
3214
  health(): Promise<unknown>;
@@ -3167,7 +3218,10 @@ declare class OwV2Client {
3167
3218
  * do not bump change_seq. Poll getWorld().updated_at for meta freshness.
3168
3219
  */
3169
3220
  getWorld(): Promise<OwWorldMeta>;
3170
- /** PATCH /world -- partial world-meta update. */
3221
+ /**
3222
+ * PATCH /world -- partial world-meta update. The element write's server-managed keys are
3223
+ * stripped, and so are `slug` and `public_page` (keel #471: set on the world page, not here).
3224
+ */
3171
3225
  patchWorld(partial: Record<string, unknown>): Promise<OwWorldMeta>;
3172
3226
  /**
3173
3227
  * GET /me -- who this world key is: the world, read or write scope, the kind (owner,
@@ -3331,11 +3385,25 @@ declare class OwV2Client {
3331
3385
  * says why the epoch last moved, `role` or `view` (see OwChangesPage.epoch_reason).
3332
3386
  * keel returns the owner's cursor as a number; it is handed back as a
3333
3387
  * string, like every cursor here.
3388
+ *
3389
+ * Conditional (keel #518, 4.57.0): keel tags the answer with an ETag that covers the head and
3390
+ * the epoch, so a view change moves it too. The client keeps the last tag and sends it as
3391
+ * If-None-Match on its next head read. When nothing moved, keel answers 304 with no body, and
3392
+ * this returns the `head`, `cursor` and `epoch` from the last 200 with `unchanged: true`
3393
+ * (no `epoch_reason`); every 200 says `unchanged: false`. keel refunds a 304 against the
3394
+ * request budget, so a poll that mostly sees nothing costs little. A poller: read the head,
3395
+ * sleep when `unchanged`, walk changesAll when `head` or `epoch` moved past what you applied.
3396
+ * The interval is yours; the client never sleeps or polls on its own. The tag is kept per
3397
+ * client instance and sent back exactly as received (`W/` and quotes included), never
3398
+ * compared; a 409 `resync_required` from this or from changes/changesAll clears it, so the
3399
+ * next head read is a full 200 (with `epoch_reason`). A 304 while no tag is held is an
3400
+ * OwApiError (status 304): the client never makes up a head.
3334
3401
  */
3335
3402
  changesHead(): Promise<{
3336
3403
  head: number;
3337
3404
  cursor: string;
3338
3405
  epoch: number | null;
3406
+ unchanged: boolean;
3339
3407
  epoch_reason?: OwResyncReason | null;
3340
3408
  }>;
3341
3409
  /**
@@ -3763,9 +3831,18 @@ declare class OwV2Client {
3763
3831
  allowEmpty?: boolean;
3764
3832
  replayAware?: boolean;
3765
3833
  /** A `change_seq` to send as `If-Match` (keel #210); a whole number, else TypeError before sending. */
3766
- /** An element write (create, put, patch, delete, links, bulk): where an agent client sends X-OW-Agent. */
3834
+ /** A write keel's document lists X-OW-Agent on (the element writes, hide and unhide, the file ticket, setMyCharacter, mintWorldKey): where an agent client sends it. */
3767
3835
  write?: boolean;
3836
+ /** One of the seven element writes keel reads X-OW-Client on (keel #520: create, put, patch, delete, links, bulk, adopt): where the client's name goes. */
3837
+ elementWrite?: boolean;
3768
3838
  ifMatch?: number;
3839
+ /** A tag to send as `If-None-Match` exactly as given (keel #518, the head read): unlike ifMatch, nothing is quoted or checked. */
3840
+ ifNoneMatch?: string;
3841
+ /**
3842
+ * Called with every response's `ETag` header (as received, null when absent) and status, before
3843
+ * the body is read. With it given, a 304 Not Modified returns null instead of throwing.
3844
+ */
3845
+ onEtag?: (etag: string | null, status: number) => void;
3769
3846
  }): Promise<T>;
3770
3847
  }
3771
3848
  /**
@@ -4806,4 +4883,4 @@ declare class TokenResource {
4806
4883
  getEncryptionInfo(): Promise<EncryptionInfo>;
4807
4884
  }
4808
4885
 
4809
- export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwDeleteReason, type OwDraught, type OwDraughtComponent, type OwDraughtItem, type OwDraughtPolicy, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, type OwHiddenRow, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, type ParseError, type ParseOptions, type ParseResult, ROOM_PAIRS, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, WORLD_FILE_MEDIA_TYPE, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, isRoomElement, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, simpleCaseFold, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
4886
+ export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwDeleteReason, type OwDraught, type OwDraughtComponent, type OwDraughtItem, type OwDraughtPolicy, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, type OwHiddenRow, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWorldPublicPage, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, type ParseError, type ParseOptions, type ParseResult, ROOM_PAIRS, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, WORLD_FILE_MEDIA_TYPE, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, isRoomElement, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, simpleCaseFold, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
package/dist/index.js CHANGED
@@ -504,6 +504,10 @@ var DEFAULT_PAGE_SIZE = 100;
504
504
  var DEFAULT_CHANGES_PAGE_SIZE = 100;
505
505
  var OwV2Client = class {
506
506
  constructor(config = {}) {
507
+ /** The conditional head poll's state (keel #518), per instance. */
508
+ this.headPoll = new HeadPollState();
509
+ only("new OwV2Client", config, CLIENT_CONFIG_KEYS);
510
+ this.clientName = clientNameOf(config.client);
507
511
  this.apiKey = config.apiKey ?? "";
508
512
  this.apiPin = config.apiPin || void 0;
509
513
  this.keyKind = config.apiKey ? detectKeyKind(config.apiKey) : "none";
@@ -529,9 +533,14 @@ var OwV2Client = class {
529
533
  async getWorld() {
530
534
  return this.request("GET", "/world/");
531
535
  }
532
- /** PATCH /world -- partial world-meta update. */
536
+ /**
537
+ * PATCH /world -- partial world-meta update. The element write's server-managed keys are
538
+ * stripped, and so are `slug` and `public_page` (keel #471: set on the world page, not here).
539
+ */
533
540
  async patchWorld(partial) {
534
- return this.request("PATCH", "/world/", { body: sanitizePayload(partial) });
541
+ const body = sanitizePayload(partial);
542
+ for (const field of WORLD_READ_ONLY_FIELDS) delete body[field];
543
+ return this.request("PATCH", "/world/", { body });
535
544
  }
536
545
  // -- Who am I, and who is here ---------------------------------------------
537
546
  /**
@@ -649,6 +658,7 @@ var OwV2Client = class {
649
658
  }
650
659
  return this.request("POST", `/${type}/`, {
651
660
  write: true,
661
+ elementWrite: true,
652
662
  body,
653
663
  idempotencyKey: opts.idempotencyKey
654
664
  });
@@ -660,7 +670,7 @@ var OwV2Client = class {
660
670
  */
661
671
  async upsert(type, id, element) {
662
672
  refuseKnownOf(element, "upsert");
663
- return this.request("PUT", `/${type}/${id}/`, { write: true, body: sanitizePayload(element) });
673
+ return this.request("PUT", `/${type}/${id}/`, { write: true, elementWrite: true, body: sanitizePayload(element) });
664
674
  }
665
675
  /**
666
676
  * PATCH /{type}/{id}/ -- partial update. DESTRUCTIVE on sent fields: arrays
@@ -675,7 +685,7 @@ var OwV2Client = class {
675
685
  async patch(type, id, partial, opts = {}) {
676
686
  only("patch", opts, ["ifMatch"]);
677
687
  refuseKnownOf(partial, "patch");
678
- return this.request("PATCH", `/${type}/${id}/`, { write: true, body: sanitizePayload(partial), ifMatch: opts.ifMatch });
688
+ return this.request("PATCH", `/${type}/${id}/`, { write: true, elementWrite: true, body: sanitizePayload(partial), ifMatch: opts.ifMatch });
679
689
  }
680
690
  /**
681
691
  * DELETE /{type}/{id}/ -- idempotent (204 on absent). Server writes a
@@ -683,7 +693,7 @@ var OwV2Client = class {
683
693
  * transaction -- no client-side unlink pass needed, ever.
684
694
  */
685
695
  async delete(type, id) {
686
- await this.request("DELETE", `/${type}/${id}/`, { write: true, allowEmpty: true });
696
+ await this.request("DELETE", `/${type}/${id}/`, { write: true, elementWrite: true, allowEmpty: true });
687
697
  }
688
698
  /**
689
699
  * POST /{type}/{id}/links/{field} with {add, remove} -- atomic link merge.
@@ -695,7 +705,7 @@ var OwV2Client = class {
695
705
  async editLinks(type, id, field, edit, opts = {}) {
696
706
  only("editLinks", opts, ["ifMatch"]);
697
707
  only("editLinks", edit, ["add", "remove"], "The edit is { add?: string[], remove?: string[] }.");
698
- return this.request("POST", `/${type}/${id}/links/${field}`, { write: true, body: edit, ifMatch: opts.ifMatch });
708
+ return this.request("POST", `/${type}/${id}/links/${field}`, { write: true, elementWrite: true, body: edit, ifMatch: opts.ifMatch });
699
709
  }
700
710
  /**
701
711
  * POST /{type}/{id}/adopt with {member} -- change who made an element (keel #427, D141).
@@ -711,7 +721,7 @@ var OwV2Client = class {
711
721
  */
712
722
  async adopt(type, id, member) {
713
723
  if (typeof member !== "string" || member === "") throw new TypeError('adopt: member must be a membership id or "owner"');
714
- return this.request("POST", `/${type}/${id}/adopt`, { write: true, body: { member } });
724
+ return this.request("POST", `/${type}/${id}/adopt`, { write: true, elementWrite: true, body: { member } });
715
725
  }
716
726
  // -- Bulk ----------------------------------------------------------------
717
727
  /**
@@ -745,6 +755,7 @@ var OwV2Client = class {
745
755
  };
746
756
  return this.request("POST", "/bulk/", {
747
757
  write: true,
758
+ elementWrite: true,
748
759
  body,
749
760
  idempotencyKey: opts.idempotencyKey,
750
761
  replayAware: true
@@ -775,7 +786,7 @@ var OwV2Client = class {
775
786
  async changes(opts = {}) {
776
787
  only("changes", opts, ["since", "limit", "after"]);
777
788
  const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize, after: opts.after });
778
- const page = await this.request("GET", "/changes/", { query });
789
+ const page = await this.headPoll.feedRead(() => this.request("GET", "/changes/", { query }));
779
790
  return { ...page, cursor: String(page.cursor) };
780
791
  }
781
792
  /**
@@ -790,10 +801,41 @@ var OwV2Client = class {
790
801
  * says why the epoch last moved, `role` or `view` (see OwChangesPage.epoch_reason).
791
802
  * keel returns the owner's cursor as a number; it is handed back as a
792
803
  * string, like every cursor here.
804
+ *
805
+ * Conditional (keel #518, 4.57.0): keel tags the answer with an ETag that covers the head and
806
+ * the epoch, so a view change moves it too. The client keeps the last tag and sends it as
807
+ * If-None-Match on its next head read. When nothing moved, keel answers 304 with no body, and
808
+ * this returns the `head`, `cursor` and `epoch` from the last 200 with `unchanged: true`
809
+ * (no `epoch_reason`); every 200 says `unchanged: false`. keel refunds a 304 against the
810
+ * request budget, so a poll that mostly sees nothing costs little. A poller: read the head,
811
+ * sleep when `unchanged`, walk changesAll when `head` or `epoch` moved past what you applied.
812
+ * The interval is yours; the client never sleeps or polls on its own. The tag is kept per
813
+ * client instance and sent back exactly as received (`W/` and quotes included), never
814
+ * compared; a 409 `resync_required` from this or from changes/changesAll clears it, so the
815
+ * next head read is a full 200 (with `epoch_reason`). A 304 while no tag is held is an
816
+ * OwApiError (status 304): the client never makes up a head.
793
817
  */
794
818
  async changesHead() {
795
- const page = await this.request("GET", "/changes/", { query: buildQuery({ head: "true" }) });
796
- const out = { head: page.head, cursor: String(page.cursor), epoch: page.epoch ?? null };
819
+ const seen = { status: 0, etag: null };
820
+ const poll = this.headPoll;
821
+ const page = await poll.feedRead(() => this.request("GET", "/changes/", {
822
+ query: buildQuery({ head: "true" }),
823
+ ifNoneMatch: poll.tag,
824
+ onEtag: (etag, status) => {
825
+ seen.etag = etag;
826
+ seen.status = status;
827
+ }
828
+ }));
829
+ if (seen.status === 304) {
830
+ if (!poll.held) {
831
+ throw new OwApiError(304, null, "GET /changes?head=true answered 304 Not Modified while nothing is held: no head read has answered 200 on this client, so there is no head to return", null, null);
832
+ }
833
+ return { ...poll.held, unchanged: true };
834
+ }
835
+ const tip = { head: page.head, cursor: String(page.cursor), epoch: page.epoch ?? null };
836
+ poll.tag = seen.etag || void 0;
837
+ poll.held = tip;
838
+ const out = { ...tip, unchanged: false };
797
839
  if (page.epoch_reason !== void 0) out.epoch_reason = page.epoch_reason;
798
840
  return out;
799
841
  }
@@ -1510,7 +1552,9 @@ var OwV2Client = class {
1510
1552
  if (opts.body !== void 0) headers["Content-Type"] = "application/json";
1511
1553
  if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1512
1554
  if (opts.ifMatch !== void 0) headers["If-Match"] = `"${opts.ifMatch}"`;
1555
+ if (opts.ifNoneMatch !== void 0) headers["If-None-Match"] = opts.ifNoneMatch;
1513
1556
  if (this.agent && opts.write) headers["X-OW-Agent"] = "1";
1557
+ if (opts.elementWrite) headers["X-OW-Client"] = this.clientName;
1514
1558
  let res;
1515
1559
  try {
1516
1560
  res = await this.fetchImpl(url, {
@@ -1521,6 +1565,10 @@ var OwV2Client = class {
1521
1565
  } catch (cause) {
1522
1566
  throw new OwNetworkError(`OnlyWorlds request failed: ${method} ${path}`, cause);
1523
1567
  }
1568
+ if (opts.onEtag) {
1569
+ opts.onEtag(res.headers?.get?.("ETag") ?? null, res.status);
1570
+ if (res.status === 304) return null;
1571
+ }
1524
1572
  if (!res.ok) throw await errorFromResponse(res);
1525
1573
  if (res.status === 204 || opts.allowEmpty) {
1526
1574
  const text = await res.text();
@@ -1533,7 +1581,22 @@ var OwV2Client = class {
1533
1581
  return parsed;
1534
1582
  }
1535
1583
  };
1584
+ var HeadPollState = class {
1585
+ /** A read of /changes: a 409 `resync_required` drops the tag and the held head, then is thrown as is. */
1586
+ async feedRead(read) {
1587
+ try {
1588
+ return await read();
1589
+ } catch (err) {
1590
+ if (err instanceof OwApiError && err.isResyncRequired) {
1591
+ this.tag = void 0;
1592
+ this.held = void 0;
1593
+ }
1594
+ throw err;
1595
+ }
1596
+ }
1597
+ };
1536
1598
  var READ_ONLY_FIELDS = ["world", "type", "created_at", "updated_at", "change_seq", "known_of"];
1599
+ var WORLD_READ_ONLY_FIELDS = ["slug", "public_page"];
1537
1600
  function sanitizePayload(payload) {
1538
1601
  if (!payload || typeof payload !== "object") return payload;
1539
1602
  const rest = { ...payload };
@@ -1670,6 +1733,16 @@ function only(method, obj, keys, hint = "") {
1670
1733
  }
1671
1734
  }
1672
1735
  }
1736
+ var CLIENT_CONFIG_KEYS = ["apiKey", "apiPin", "baseUrl", "pageSize", "changesPageSize", "fetch", "agent", "client"];
1737
+ var CLIENT_NAME_RULE = /^[a-z0-9-]{1,32}$/;
1738
+ var DEFAULT_CLIENT_NAME = "sdk";
1739
+ function clientNameOf(value) {
1740
+ if (value === void 0) return DEFAULT_CLIENT_NAME;
1741
+ if (typeof value !== "string" || !CLIENT_NAME_RULE.test(value)) {
1742
+ throw new TypeError(`OwV2Client: client must be lower case letters, digits and hyphens, 1 to 32 characters ([a-z0-9-]{1,32}), such as 'atlas'; got ${JSON.stringify(value) ?? String(value)}. It is sent as X-OW-Client on writes; leave it out for '${DEFAULT_CLIENT_NAME}'.`);
1743
+ }
1744
+ return value;
1745
+ }
1673
1746
  function buildQuery(params) {
1674
1747
  const q = new URLSearchParams();
1675
1748
  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.55.0",
3
+ "version": "4.57.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {