@onlyworlds/sdk 4.11.0 → 4.12.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
@@ -40,6 +40,9 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
40
40
  - Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
41
41
  to the edge with a single-use ticket from keel, never with the API key. `removeImage(urlOrKey)`
42
42
  takes one back (its uploader or the world's owner); it leaves `image_url` on elements as is.
43
+ - Invite links (since 4.12.0): `previewJoin(code)` (no credential sent) shows who invites, to
44
+ which world, in what role; `acceptJoin(code)` with an `ow_a_` account token joins. Bad code:
45
+ `err.isNotFound`; `err.isAlreadyMember`, `err.isPinRequired` for the two 409s.
43
46
  - Lookup without a model (since 4.11.0): `buildWorldIndex(elements)` then
44
47
  `lookupWorld(index, question)` ranks the elements a question is about (names, then kind, one
45
48
  link step, then text); `renderElement(el, index)` is a compact card with links by name.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.12.0] — 2026-10-12
9
+
10
+ ### Added
11
+ - **Joining a world by link (keel #212).** `previewJoin(code)` reads what a join code holds
12
+ without using it (`OwJoinPreview`: `reader`, `world`, `invited_by`, `owner_character`,
13
+ `role`, `with_character`, `expires_at`) and sends no credential, whatever key the client
14
+ holds. `acceptJoin(code)` accepts an invite link as the account behind the client's `ow_a_`
15
+ token and returns the new membership (`OwJoinedMembership`, `roster_character` included);
16
+ a world key is a `TypeError` before anything is sent. New getters on `OwApiError`:
17
+ `isNotFound` (every bad code is one 404), `isAlreadyMember` and `isPinRequired` (the two
18
+ 409s, told apart by code). Shapes read from keel's code (`accounts/join_api.py`,
19
+ `accounts/api.py`), since the OpenAPI document gives no response bodies for these routes.
20
+ Checked live: a bad code on production answers `isNotFound`. A real accept has not been
21
+ run from here (no account token); Lantern's door is its first live use.
22
+
8
23
  ## [4.11.0] — 2026-10-12
9
24
 
10
25
  ### Added
package/README.md CHANGED
@@ -110,6 +110,17 @@ await writer.patch('character', id, { image_url: null });
110
110
 
111
111
  The image's uploader or the world's owner can remove an uploaded image; the space goes back to the uploader's storage. Removal leaves elements as they are, so clear their `image_url` yourself (`createRemovalTicket(key).referenced` says how many still show it). A copy the network already holds can keep answering for a while.
112
112
 
113
+ ## Invite links
114
+
115
+ A world's owner can invite a person with a link (`https://www.onlyworlds.com/join#ow_j_…`). The code is the part after `#`:
116
+
117
+ ```typescript
118
+ const invite = await client.previewJoin(code); // who invites, which world, what role; sends no key
119
+ const membership = await account.acceptJoin(code); // account = a client with an ow_a_ token
120
+ ```
121
+
122
+ A bad code (unknown, used, revoked, expired) is one 404 (`err.isNotFound`). Accepting can answer `err.isAlreadyMember` or `err.isPinRequired` (set a PIN first); a refusal never uses up the code.
123
+
113
124
  ## Sync
114
125
 
115
126
  ```typescript
package/dist/index.d.ts CHANGED
@@ -1988,6 +1988,37 @@ interface OwRemovalTicket {
1988
1988
  referenced: number;
1989
1989
  }
1990
1990
  /** The edge's 200 for one removal. */
1991
+ /** POST /join/preview: what a join code holds, read without using it (keel #212). */
1992
+ interface OwJoinPreview {
1993
+ /** `person`: an invite link, accepted with acceptJoin. `agent`: redeemed by an AI agent at POST /join. */
1994
+ reader: 'person' | 'agent' | (string & {});
1995
+ world: {
1996
+ name: string;
1997
+ description: string | null;
1998
+ };
1999
+ /** The name the inviter signed the link with, or null. */
2000
+ invited_by: string | null;
2001
+ /** The world owner's chosen Character's name, never a username; null when none is set. */
2002
+ owner_character: string | null;
2003
+ role: 'co-builder' | 'contributor' | 'guest' | (string & {});
2004
+ /** Whether joining gives the joiner a Character of their own (agent links). */
2005
+ with_character: boolean | null;
2006
+ /** ISO 8601, UTC. */
2007
+ expires_at: string;
2008
+ [k: string]: unknown;
2009
+ }
2010
+ /** POST /account/join: the new membership (the same row as accepting an email invite). */
2011
+ interface OwJoinedMembership {
2012
+ /** The membership's id. */
2013
+ id: string;
2014
+ world_id: string;
2015
+ world_name: string;
2016
+ role: 'co-builder' | 'contributor' | 'guest' | (string & {});
2017
+ joined_at: string | null;
2018
+ /** The id of the joiner's roster Character, or null. */
2019
+ roster_character: string | null;
2020
+ [k: string]: unknown;
2021
+ }
1991
2022
  interface OwRemovedImage {
1992
2023
  /** The object key that was removed. */
1993
2024
  removed: string;
@@ -2139,6 +2170,12 @@ declare class OwApiError extends Error {
2139
2170
  * is now, so no second read is needed.
2140
2171
  */
2141
2172
  get isPreconditionFailed(): boolean;
2173
+ /** 404 `not_found`: no such element or route, or a join code that is unknown, used, revoked or expired. */
2174
+ get isNotFound(): boolean;
2175
+ /** acceptJoin: the account is already a member of that world (409 `already_member`). The code is not spent. */
2176
+ get isAlreadyMember(): boolean;
2177
+ /** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
2178
+ get isPinRequired(): boolean;
2142
2179
  }
2143
2180
  /**
2144
2181
  * 412 `precondition_failed` (keel #210): the element changed since the `change_seq` sent as
@@ -2368,6 +2405,23 @@ declare class OwV2Client {
2368
2405
  removeImage(keyOrUrl: string, opts?: {
2369
2406
  ticket?: OwRemovalTicket;
2370
2407
  }): Promise<OwRemovedImage>;
2408
+ /**
2409
+ * POST /join/preview -- what a join code holds, without using it: who invites, to
2410
+ * which world, in what role, and `reader` (`person`: accept it with acceptJoin;
2411
+ * `agent`: an AI agent redeems it at POST /join). Sends no credential, whatever key
2412
+ * this client holds. Every bad code (unknown, used, revoked, expired) is the same
2413
+ * 404 `not_found` (`err.isNotFound`). `code` is the part of the link after `#`.
2414
+ */
2415
+ previewJoin(code: string): Promise<OwJoinPreview>;
2416
+ /**
2417
+ * POST /account/join -- accept an invite link's code as the account this client's
2418
+ * `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
2419
+ * (every bad code, and a code made for an AI agent), 409 `already_member`
2420
+ * (`err.isAlreadyMember`) and 409 `pin_required` (`err.isPinRequired`: the account
2421
+ * has no PIN yet). A refusal never spends the code. A world key here is a TypeError
2422
+ * before anything is sent.
2423
+ */
2424
+ acceptJoin(code: string): Promise<OwJoinedMembership>;
2371
2425
  /**
2372
2426
  * Raw authenticated request against this client's baseUrl. Public since 4.0
2373
2427
  * so auxiliary resources can ride the same transport — it structurally
@@ -3009,4 +3063,4 @@ declare class TokenResource {
3009
3063
  getEncryptionInfo(): Promise<EncryptionInfo>;
3010
3064
  }
3011
3065
 
3012
- 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 OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMediaTicket, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldMeta, PHOTO_MAX_BYTES, PHOTO_TYPES, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
3066
+ 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 OwJoinPreview, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMediaTicket, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldMeta, PHOTO_MAX_BYTES, PHOTO_TYPES, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
package/dist/index.js CHANGED
@@ -75,6 +75,18 @@ var OwApiError = class extends Error {
75
75
  get isPreconditionFailed() {
76
76
  return this.status === 412 && this.code === "precondition_failed";
77
77
  }
78
+ /** 404 `not_found`: no such element or route, or a join code that is unknown, used, revoked or expired. */
79
+ get isNotFound() {
80
+ return this.status === 404 && this.code === "not_found";
81
+ }
82
+ /** acceptJoin: the account is already a member of that world (409 `already_member`). The code is not spent. */
83
+ get isAlreadyMember() {
84
+ return this.status === 409 && this.code === "already_member";
85
+ }
86
+ /** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
87
+ get isPinRequired() {
88
+ return this.status === 409 && this.code === "pin_required";
89
+ }
78
90
  };
79
91
  var OwPreconditionFailedError = class extends OwApiError {
80
92
  constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
@@ -410,6 +422,31 @@ var OwV2Client = class {
410
422
  if (!res.ok) throw await errorFromResponse(res);
411
423
  return await res.json();
412
424
  }
425
+ // -- Joining a world by link (keel #212) ----------------------------------
426
+ /**
427
+ * POST /join/preview -- what a join code holds, without using it: who invites, to
428
+ * which world, in what role, and `reader` (`person`: accept it with acceptJoin;
429
+ * `agent`: an AI agent redeems it at POST /join). Sends no credential, whatever key
430
+ * this client holds. Every bad code (unknown, used, revoked, expired) is the same
431
+ * 404 `not_found` (`err.isNotFound`). `code` is the part of the link after `#`.
432
+ */
433
+ async previewJoin(code) {
434
+ return this.request("POST", "/join/preview", { auth: false, body: { code } });
435
+ }
436
+ /**
437
+ * POST /account/join -- accept an invite link's code as the account this client's
438
+ * `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
439
+ * (every bad code, and a code made for an AI agent), 409 `already_member`
440
+ * (`err.isAlreadyMember`) and 409 `pin_required` (`err.isPinRequired`: the account
441
+ * has no PIN yet). A refusal never spends the code. A world key here is a TypeError
442
+ * before anything is sent.
443
+ */
444
+ async acceptJoin(code) {
445
+ if (this.keyKind !== "account") {
446
+ throw new TypeError("acceptJoin needs an account token (ow_a_): a world key cannot join a world");
447
+ }
448
+ return this.request("POST", "/account/join", { body: { code } });
449
+ }
413
450
  // -- Core request machinery ----------------------------------------------
414
451
  /**
415
452
  * 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.11.0",
3
+ "version": "4.12.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {