@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 +10 -3
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/SCHEMA.md +1 -1
- package/dist/index.d.ts +81 -4
- package/dist/index.js +83 -10
- package/package.json +1 -1
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.
|
|
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
|
|
34
|
-
when `head` moved past
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
796
|
-
const
|
|
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)) {
|