@onlyworlds/sdk 4.4.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 +5 -2
- package/CHANGELOG.md +54 -1
- package/README.md +17 -0
- package/dist/index.d.ts +92 -5
- package/dist/index.js +66 -0
- package/package.json +1 -1
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:
|
|
35
|
-
|
|
34
|
+
- List filters: `name` (exact), `name__icontains`, `supertype`, `subtype`, and
|
|
35
|
+
`characters=<id>` on types with a `characters` link. Any other filter key — and
|
|
36
|
+
`?ordering=` — returns 422 naming the accepted ones. Sort client-side.
|
|
37
|
+
- Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
|
|
38
|
+
to the edge with a single-use ticket from keel, never with the API key.
|
|
36
39
|
- Ids: the client mints **UUIDv7** on an id-less `create` (since 4.2.0; keel mints v7 too).
|
|
37
40
|
A v4 or v7 id you supply is accepted. **Never sort elements by id**: worlds mix v7, v4
|
|
38
41
|
and legacy `06x…` ids (nibble 7 too, but seconds-first). For creation order use
|
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,60 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
-
## [4.
|
|
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
|
|
44
|
+
|
|
45
|
+
What the staging write probes found on the wire (Python package, 2026-10-04): one new getter, and
|
|
46
|
+
three behaviours the server has always had that no client doc said. Additive; nothing is removed.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- **`OwApiError.isOwnerOnly`**: 403 `owner_only`, a contributor attempting a world-level change
|
|
50
|
+
(`patchWorld`, publishing a show page). Owners and co-builders never see it. keel's OpenAPI
|
|
51
|
+
document does not name the code yet (Skeld has that to close); the wire returns it.
|
|
52
|
+
|
|
53
|
+
### Documented
|
|
54
|
+
- **An atomic `/bulk` rollback still returns 201 slots** for the items that would have
|
|
55
|
+
succeeded, with the ids and timestamps they would have had (keel spec: "counterfactual 201s").
|
|
56
|
+
After `atomic: true` with `errors: true`, nothing was written. On `bulk()`, `OwBulkItemResult`
|
|
57
|
+
and the README.
|
|
58
|
+
- **`name` is required as a key only**: `''` and `null` are accepted and stored as `''` (keel
|
|
59
|
+
ruling 2026-09-28, spec §6; nameless Markers exist). On `create()` and the README.
|
|
60
|
+
|
|
61
|
+
## [4.4.0] — 2026-10-04
|
|
9
62
|
|
|
10
63
|
Membership, as far as the server has shipped it: who created an element, and the refusal a
|
|
11
64
|
contributor meets on someone else's. Additive; nothing is removed or narrows.
|
package/README.md
CHANGED
|
@@ -45,6 +45,8 @@ const location = await writer.create('location', {
|
|
|
45
45
|
name: 'Dragon Peak',
|
|
46
46
|
description: 'A treacherous mountain peak where dragons nest',
|
|
47
47
|
}); // id minted client-side when omitted (retries stay idempotent)
|
|
48
|
+
// name is the one required field, and required means the key is present:
|
|
49
|
+
// '' and null are accepted and stored as '' (nameless Markers exist).
|
|
48
50
|
|
|
49
51
|
const dragon = await writer.create('creature', {
|
|
50
52
|
name: 'Vorrath the Ember-Scaled',
|
|
@@ -84,8 +86,23 @@ if (res.errors) {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
// Pass { atomic: true } for all-or-nothing instead. res.wasReplay flags idempotent replays.
|
|
89
|
+
// After a failed atomic batch NOTHING was written, but the slots that would have succeeded
|
|
90
|
+
// still say 201 (keel's counterfactual 201s): do not record those ids as created.
|
|
87
91
|
```
|
|
88
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
|
+
|
|
89
106
|
## Sync
|
|
90
107
|
|
|
91
108
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -1912,6 +1912,9 @@ interface OwBulkItem {
|
|
|
1912
1912
|
* created_at/updated_at, error slots carry an OwErrorBody under `error`.
|
|
1913
1913
|
* A contributor's write on someone else's element fails in its slot as
|
|
1914
1914
|
* `{status: 403, error: {code: 'not_author'}}` (keel D72), never as a thrown error.
|
|
1915
|
+
* After an `atomic: true` request with `errors` true NOTHING was written, yet the slots of
|
|
1916
|
+
* the items that would have succeeded still say 201 (keel's "counterfactual 201s"): do not
|
|
1917
|
+
* record their ids as created.
|
|
1915
1918
|
*/
|
|
1916
1919
|
interface OwBulkItemResult {
|
|
1917
1920
|
status: number;
|
|
@@ -1944,6 +1947,41 @@ interface OwLinkEdit {
|
|
|
1944
1947
|
add?: string[];
|
|
1945
1948
|
remove?: string[];
|
|
1946
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
|
+
}
|
|
1947
1985
|
interface ListParams {
|
|
1948
1986
|
limit?: number;
|
|
1949
1987
|
cursor?: string;
|
|
@@ -1952,9 +1990,11 @@ interface ListParams {
|
|
|
1952
1990
|
/** Sparse include-set of field names. */
|
|
1953
1991
|
fields?: string[];
|
|
1954
1992
|
/**
|
|
1955
|
-
* Accepted: `name__icontains`, `supertype`, `subtype
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
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
|
|
1958
1998
|
* `__isnull` are designed in keel's spec but not built; `ordering` 422s too.)
|
|
1959
1999
|
*/
|
|
1960
2000
|
filter?: Record<string, string | number | boolean>;
|
|
@@ -1994,7 +2034,12 @@ interface OwClientConfig {
|
|
|
1994
2034
|
* envelope also carries `type` and `param` (fixtures P4a/P2c); both are
|
|
1995
2035
|
* surfaced on the thrown error.
|
|
1996
2036
|
*/
|
|
1997
|
-
/**
|
|
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
|
+
*/
|
|
1998
2043
|
type OwAuthErrorCode = 'invalid_credentials' | 'key_revoked' | 'world_gone';
|
|
1999
2044
|
declare class OwApiError extends Error {
|
|
2000
2045
|
readonly status: number;
|
|
@@ -2043,6 +2088,13 @@ declare class OwApiError extends Error {
|
|
|
2043
2088
|
* slot `{status: 403, error: {code: 'not_author'}}`. Owners and co-builders never see it.
|
|
2044
2089
|
*/
|
|
2045
2090
|
get isNotAuthor(): boolean;
|
|
2091
|
+
/**
|
|
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.
|
|
2096
|
+
*/
|
|
2097
|
+
get isOwnerOnly(): boolean;
|
|
2046
2098
|
}
|
|
2047
2099
|
/** Network-level failure (fetch rejected) -- no envelope to parse. */
|
|
2048
2100
|
declare class OwNetworkError extends Error {
|
|
@@ -2126,6 +2178,8 @@ declare class OwV2Client {
|
|
|
2126
2178
|
* POST /{type}/ -- create. Mints an RFC 9562 UUIDv7 for element.id when the
|
|
2127
2179
|
* caller omits one (design ruling D29d) so a retry carrying the same
|
|
2128
2180
|
* Idempotency-Key is structurally safe. Callers MAY still supply their own id.
|
|
2181
|
+
* `name` is the one required field, and required means the key is present: `''` and
|
|
2182
|
+
* `null` are accepted and stored as `''` (nameless Markers exist; keel ruling 2026-09-28).
|
|
2129
2183
|
*/
|
|
2130
2184
|
create(type: ElementType | string, element: OwElement | Record<string, unknown>, opts?: {
|
|
2131
2185
|
idempotencyKey?: string;
|
|
@@ -2159,6 +2213,9 @@ declare class OwV2Client {
|
|
|
2159
2213
|
* Success slots echo server-authoritative timestamps: set your sync baseline
|
|
2160
2214
|
* from this response alone. When an idempotencyKey is replayed, the returned
|
|
2161
2215
|
* response carries wasReplay:true (read from the Idempotent-Replay header).
|
|
2216
|
+
* After an atomic:true request with errors:true NOTHING was written, yet the slots of
|
|
2217
|
+
* the items that would have succeeded still say 201 with the ids and timestamps they
|
|
2218
|
+
* would have had (keel's "counterfactual 201s"): do not record those ids as created.
|
|
2162
2219
|
*/
|
|
2163
2220
|
bulk(items: OwBulkItem[], opts?: {
|
|
2164
2221
|
atomic?: boolean;
|
|
@@ -2185,6 +2242,36 @@ declare class OwV2Client {
|
|
|
2185
2242
|
cursor: string;
|
|
2186
2243
|
head: number;
|
|
2187
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>;
|
|
2188
2275
|
/**
|
|
2189
2276
|
* Raw authenticated request against this client's baseUrl. Public since 4.0
|
|
2190
2277
|
* so auxiliary resources can ride the same transport — it structurally
|
|
@@ -2559,4 +2646,4 @@ declare class TokenResource {
|
|
|
2559
2646
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
2560
2647
|
}
|
|
2561
2648
|
|
|
2562
|
-
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
|
@@ -48,6 +48,15 @@ var OwApiError = class extends Error {
|
|
|
48
48
|
get isNotAuthor() {
|
|
49
49
|
return this.status === 403 && this.code === "not_author";
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
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.
|
|
56
|
+
*/
|
|
57
|
+
get isOwnerOnly() {
|
|
58
|
+
return this.status === 403 && this.code === "owner_only";
|
|
59
|
+
}
|
|
51
60
|
};
|
|
52
61
|
var OwNetworkError = class extends Error {
|
|
53
62
|
constructor(message, cause) {
|
|
@@ -182,6 +191,8 @@ var OwV2Client = class {
|
|
|
182
191
|
* POST /{type}/ -- create. Mints an RFC 9562 UUIDv7 for element.id when the
|
|
183
192
|
* caller omits one (design ruling D29d) so a retry carrying the same
|
|
184
193
|
* Idempotency-Key is structurally safe. Callers MAY still supply their own id.
|
|
194
|
+
* `name` is the one required field, and required means the key is present: `''` and
|
|
195
|
+
* `null` are accepted and stored as `''` (nameless Markers exist; keel ruling 2026-09-28).
|
|
185
196
|
*/
|
|
186
197
|
async create(type, element, opts = {}) {
|
|
187
198
|
const body = sanitizePayload(element);
|
|
@@ -231,6 +242,9 @@ var OwV2Client = class {
|
|
|
231
242
|
* Success slots echo server-authoritative timestamps: set your sync baseline
|
|
232
243
|
* from this response alone. When an idempotencyKey is replayed, the returned
|
|
233
244
|
* response carries wasReplay:true (read from the Idempotent-Replay header).
|
|
245
|
+
* After an atomic:true request with errors:true NOTHING was written, yet the slots of
|
|
246
|
+
* the items that would have succeeded still say 201 with the ids and timestamps they
|
|
247
|
+
* would have had (keel's "counterfactual 201s"): do not record those ids as created.
|
|
234
248
|
*/
|
|
235
249
|
async bulk(items, opts = {}) {
|
|
236
250
|
const body = {
|
|
@@ -271,6 +285,58 @@ var OwV2Client = class {
|
|
|
271
285
|
} while (page.has_more);
|
|
272
286
|
return { cursor: page.cursor, head: page.head };
|
|
273
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
|
+
}
|
|
274
340
|
// -- Core request machinery ----------------------------------------------
|
|
275
341
|
/**
|
|
276
342
|
* Raw authenticated request against this client's baseUrl. Public since 4.0
|