@onlyworlds/sdk 4.48.1 → 4.50.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 +4 -1
- package/CHANGELOG.md +23 -0
- package/README.md +18 -3
- package/dist/index.d.ts +77 -32
- package/dist/index.js +381 -72
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -79,7 +79,10 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
|
|
|
79
79
|
- World files (no account needed; since 4.10.0): `readWorldFile(bytes)` opens a `.zip` holding one
|
|
80
80
|
world folder and returns `{ worldId, name, world, files: [{ path, data }] }`;
|
|
81
81
|
`writeWorldFile(files, topDir)` makes one. Refusals throw `OwWorldFileError` with a `reason`.
|
|
82
|
-
`world.json`'s `api` block (link and key) is removed both ways.
|
|
82
|
+
`world.json`'s `api` block (link and key) is removed both ways. Both follow Folder Format
|
|
83
|
+
§4.7 (`.owworld`): the reader checks the central directory before inflating
|
|
84
|
+
and ignores files beside the world's folder; the writer's output is byte-stable (marker
|
|
85
|
+
first, UTF-8 name order, 1980 stamps) and holds only `world.json` and the folder's parts.
|
|
83
86
|
- World calendars (since 4.20.0): `worldCalendars(elements)` finds the elements carrying
|
|
84
87
|
`atlas_calendar` (Atlas owns the field: read it, never write it); `resolveCalendarLens` picks
|
|
85
88
|
one; `formatStep(config, step)` names a point on the world's integer time line, `parseDate`
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.50.0] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `setMyCharacter(characterId | null)`: `PUT /me/character`, a person member's pick of the roster Character that is them in this world (keel PR 160, #533); answers the `/me` body. `err.isCharacterHidden` for the 422 `character_hidden` it and the member PATCH answer.
|
|
13
|
+
- `hidden?: true` on every element body (`OwElementBase`): present only when the owner or a co-builder hid the element and this reader still sees it (keel #537); never on other readers' bodies, ignored in a write.
|
|
14
|
+
- `site` joins `OwWriteDoor`: a write or delete the owner made signed in on onlyworlds.com (keel #530).
|
|
15
|
+
|
|
16
|
+
## [4.49.0] - 2026-10-22
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **The world file follows Folder Format §4.7, the `.owworld` archive** (OW Council #522), and gives the spec's verdict and cause on every one of the 74 fixtures in its conformance set (Quillon's, `owworld-fixtures`), plus its three on-demand floor cases.
|
|
21
|
+
- The reader parses the central directory itself (zip64 included) and runs every check on names, kinds, methods and sizes before it inflates anything.
|
|
22
|
+
- Six new refusal causes join `OwWorldFileReason`: `duplicate` (OW-17), `collision` (OW-18: NFC, then simple case folding), `link` (OW-20), `method` (OW-21), `name` (OW-22) and `marker` (OW-6). A `Record<OwWorldFileReason, …>` in a caller needs the six new keys.
|
|
23
|
+
- The OW-18 fold is the C and S rows of Unicode's CaseFolding.txt 15.1, built in as a table, never the runtime's casing, so a fold a newer Unicode adds stays out until the pin moves (Skeld's ruling, OW Council #9830). `simpleCaseFold(text)` exports it.
|
|
24
|
+
- Files beside the world's folder are ignored, no longer `more_than_one` (OW-4). A `world.json` under `records/` is a producer's data, not a second world (OW-5). A spanned archive is `not_openable`. An entry that inflates past the size it declares is `too_big` at any budget (OW-14).
|
|
25
|
+
- The default budget is the spec's floor: `WORLD_FILE_MAX_BYTES` 2 GiB (was 1 GiB), `WORLD_FILE_MAX_ENTRIES` 500,000 (was 200,000) (OW-15).
|
|
26
|
+
- A `mimetype` marker is never required. One with other bytes than `WORLD_FILE_MEDIA_TYPE` (`application/vnd.onlyworlds.world+zip`, newly exported) is refused, and the marker is never returned as a file.
|
|
27
|
+
- `files` come back in code point order (the UTF-8 bytes of the name), not UTF-16 order.
|
|
28
|
+
- The writer gives one set of bytes for one input (OW-12, OW-13). It places the `mimetype` marker first and stored, orders entries by the UTF-8 bytes of their names, and stamps every entry 1980-01-01 00:00 (was the clock).
|
|
29
|
+
- The writer writes only `world.json`, `elements/`, `spatial/`, `media/`, `records/` and a tool's dot-folder it is handed (OW-7). It leaves out any other file, and a dot-name or `.meta` inside a content folder. It refuses a `name` or a `collision` as the reader would.
|
|
30
|
+
|
|
8
31
|
## [4.48.1] - 2026-10-22
|
|
9
32
|
|
|
10
33
|
### Changed
|
package/README.md
CHANGED
|
@@ -240,19 +240,34 @@ const { token_rating, tokens_remaining, resets_at } = await account.allowance();
|
|
|
240
240
|
|
|
241
241
|
## World files
|
|
242
242
|
|
|
243
|
-
A world file
|
|
243
|
+
A world file (`.owworld`, a `.zip` by another name) is one archive holding a whole world folder (`world.json` and `elements/<type>/*.json`, as Atlas keeps a world on disk), the way Atlas downloads and opens one. It needs no account and works in the browser and in Node. Both functions follow the Folder Format spec's §4.7 and pass its fixture set.
|
|
244
244
|
|
|
245
245
|
```typescript
|
|
246
246
|
import { readWorldFile, writeWorldFile, OwWorldFileError } from '@onlyworlds/sdk';
|
|
247
247
|
|
|
248
248
|
const world = readWorldFile(bytes); // a Uint8Array
|
|
249
249
|
world.worldId; world.name; world.world; // world.json, parsed
|
|
250
|
-
world.files; // [{ path, data }], relative to the world's folder,
|
|
250
|
+
world.files; // [{ path, data }], relative to the world's folder, in code point order
|
|
251
251
|
|
|
252
252
|
const zip = writeWorldFile(world.files, 'hollow-reach-0192a000'); // one top-level folder
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
The reader
|
|
255
|
+
The reader checks every entry before it inflates anything, and refuses the file before returning anything. Check `err.reason` on the `OwWorldFileError` it throws:
|
|
256
|
+
|
|
257
|
+
| `reason` | what the file holds |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `not_openable` | not a zip, a spanned one, or a broken entry |
|
|
260
|
+
| `unsafe_path` | a path that could land outside the folder, or a path that is both a file and a folder |
|
|
261
|
+
| `name` | a name some disk cannot hold: `CON` and the other device names, a trailing dot or space, `<>:"\|?*`, a control character, a non-ASCII name without the UTF-8 flag |
|
|
262
|
+
| `duplicate`, `collision` | two entries with one name, or two names that differ only in case or in how an accent is written |
|
|
263
|
+
| `link`, `method` | a link or special file, an encrypted entry, or a method other than stored or deflate |
|
|
264
|
+
| `too_big`, `too_many` | more than 2 GiB or 500,000 files uncompressed (`maxBytes`, `maxEntries` to change them), or an entry bigger than it says |
|
|
265
|
+
| `marker` | a `mimetype` entry with other bytes than `WORLD_FILE_MEDIA_TYPE` (a marker is never required) |
|
|
266
|
+
| `not_a_world`, `more_than_one` | no `world.json` with an `id`, two worlds, or a second `world.json` inside the world (one under `records/` is data) |
|
|
267
|
+
|
|
268
|
+
Files beside the world's folder and archive junk (`__MACOSX`, `.DS_Store`) are ignored. The world's OnlyWorlds link and key (`world.json`'s `api`) never travel: both functions remove them. A tool's own folder (`.atlas/` and the like) comes back like any other file; pass `skip: path => …` to leave files out.
|
|
269
|
+
|
|
270
|
+
The writer gives the same bytes for the same files: the `mimetype` marker first, then every entry in the UTF-8 order of its name, stamped 1980-01-01 00:00. It writes `world.json`, `elements/`, `spatial/`, `media/` and `records/`, and leaves any other file out. A tool's dot-folder you hand it travels too, which makes the file a private copy rather than a portable `.owworld`.
|
|
256
271
|
|
|
257
272
|
## World calendars
|
|
258
273
|
|
package/dist/index.d.ts
CHANGED
|
@@ -29,8 +29,10 @@ interface OwElementBase {
|
|
|
29
29
|
created_by?: string | null;
|
|
30
30
|
/** The membership whose write last changed this element (server-managed, read-only); null for the world's owner and for anything last changed before writes recorded it. */
|
|
31
31
|
changed_by?: string | null;
|
|
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`,
|
|
33
|
-
changed_via?: 'seat' | 'mcp' | 'declared' | 'api' | 'v1' | 'sync' | 'server' | null;
|
|
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
|
+
changed_via?: 'seat' | 'mcp' | 'declared' | 'api' | 'v1' | 'sync' | 'server' | 'site' | null;
|
|
34
|
+
/** 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
|
+
hidden?: true;
|
|
34
36
|
/** 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(). */
|
|
35
37
|
known_of?: true;
|
|
36
38
|
/** Namespaced extension fields (atlas_* / shadow_* / x_*), returned verbatim. */
|
|
@@ -2920,6 +2922,11 @@ declare class OwApiError extends Error {
|
|
|
2920
2922
|
* previewJoin, whose `characters` mark each `taken`. The code is not spent.
|
|
2921
2923
|
*/
|
|
2922
2924
|
get isCharacterTaken(): boolean;
|
|
2925
|
+
/**
|
|
2926
|
+
* setMyCharacter, or the member PATCH's `character` (keel #421, PR 160): the picked Character is
|
|
2927
|
+
* hidden (422 `character_hidden`, `param` character). Unhide it first, or pick another.
|
|
2928
|
+
*/
|
|
2929
|
+
get isCharacterHidden(): boolean;
|
|
2923
2930
|
/**
|
|
2924
2931
|
* The `character` sent was refused (422 `invalid_request`, `param` character). On redeemJoin
|
|
2925
2932
|
* at a table (keel #422 PR 2): the pick is off the link's list, hidden or deleted since the
|
|
@@ -3148,6 +3155,14 @@ declare class OwV2Client {
|
|
|
3148
3155
|
* creates; null for the owner) and the member's Character. A world key's call; an account
|
|
3149
3156
|
* token is a TypeError before sending.
|
|
3150
3157
|
*/
|
|
3158
|
+
/**
|
|
3159
|
+
* PUT /me/character -- pick the roster Character that is you in this world, or none (keel PR 160,
|
|
3160
|
+
* #533). A person member's write key with its PIN; answers GET /me's body. 422 for a hidden
|
|
3161
|
+
* Character (`character_hidden`, `err.isCharacterHidden`), a guest's pick it did not make, or a
|
|
3162
|
+
* taken Character (`character_taken`); 404 for one this key cannot see; 403 for an agent seat, a
|
|
3163
|
+
* player, the owner (who sets it on the world page) or a read key. A guest's view resets once.
|
|
3164
|
+
*/
|
|
3165
|
+
setMyCharacter(character: string | null): Promise<OwMe>;
|
|
3151
3166
|
me(): Promise<OwMe>;
|
|
3152
3167
|
/**
|
|
3153
3168
|
* GET /members -- the world's roster: each member's id (null for the owner), kind, role,
|
|
@@ -3842,33 +3857,52 @@ declare function getElementIcon(type: string): string;
|
|
|
3842
3857
|
declare function getElementLabel(elementType: ElementType): string;
|
|
3843
3858
|
|
|
3844
3859
|
/**
|
|
3845
|
-
* The world file
|
|
3846
|
-
*
|
|
3847
|
-
* back. Absorbed from Atlas's
|
|
3848
|
-
* 1.17), so Atlas, Lantern and any
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
* on disk, except the OnlyWorlds link and its key (`world.json` without `api`).
|
|
3852
|
-
* JSON is deflated, everything else stored. A world.json at the root, with no
|
|
3853
|
-
* folder, reads the same.
|
|
3860
|
+
* The world file, `.owworld` (Folder Format spec §4.7; a `.zip` by another
|
|
3861
|
+
* name): one archive holding one whole world as a portable copy (§4.1), and
|
|
3862
|
+
* the reader that opens one back. Absorbed from Atlas's
|
|
3863
|
+
* `src/store/world-file.ts` (Temper's, Atlas 1.17), so Atlas, Lantern and any
|
|
3864
|
+
* script read one file the same way; brought to §4.7's checks OW-1 to OW-22
|
|
3865
|
+
* and their fixture set (OW Council #522).
|
|
3854
3866
|
*
|
|
3855
3867
|
* THE READER takes bytes and returns bytes: no DOM, `File` or file system, so
|
|
3856
|
-
* it runs in a browser and in Node alike. It
|
|
3857
|
-
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3861
|
-
*
|
|
3862
|
-
*
|
|
3863
|
-
*
|
|
3868
|
+
* it runs in a browser and in Node alike. It reads the central directory
|
|
3869
|
+
* itself (zip64 included) and runs every check on names, kinds, methods and
|
|
3870
|
+
* sizes before it inflates anything. Each refusal names one cause
|
|
3871
|
+
* (`OwWorldFileReason`), checked in the reference checker's order:
|
|
3872
|
+
* - `not_openable`: not a zip, a spanned one, a broken entry or CRC.
|
|
3873
|
+
* - `unsafe_path` (OW-16, OW-19): a name that is not a plain relative path
|
|
3874
|
+
* (`\`, an empty, `.` or `..` segment, absolute, a drive), or a path that is
|
|
3875
|
+
* both a file and a folder.
|
|
3876
|
+
* - `link` (OW-20): a Unix mode that is not a regular file or a directory.
|
|
3877
|
+
* - `method` (OW-21): encrypted, or a method other than stored or deflate.
|
|
3878
|
+
* - `name` (OW-22): a Windows device name, a trailing dot or space, one of
|
|
3879
|
+
* `<>:"|?*` or a control character, a non-ASCII name without the UTF-8 flag.
|
|
3880
|
+
* - `duplicate` (OW-17), `collision` (OW-18: NFC, then simple case folding).
|
|
3881
|
+
* - `too_many`, `too_big` (OW-14, OW-15): past the budget by count or declared
|
|
3882
|
+
* size, or an entry that inflates past the size it declares.
|
|
3883
|
+
* - `marker` (OW-6): a root `mimetype` entry with other bytes than the media
|
|
3884
|
+
* type. A marker is never required.
|
|
3885
|
+
* - `not_a_world`, `more_than_one` (OW-2, OW-3, OW-5): no world, two worlds, a
|
|
3886
|
+
* second `world.json` inside the world (one under `records/` is a
|
|
3887
|
+
* producer's data), a `world.json` that will not parse or has no string id.
|
|
3888
|
+
* Files beside the world's folder and archive junk (`__MACOSX`, `.DS_Store`,
|
|
3889
|
+
* `Thumbs.db`) are ignored (OW-4), still checked and counted.
|
|
3890
|
+
*
|
|
3891
|
+
* THE WRITER emits a portable copy (OW-7, OW-12, OW-13): the `mimetype` marker
|
|
3892
|
+
* first and stored, then only the folder's parts, every entry stamped
|
|
3893
|
+
* 1980-01-01 00:00, in the UTF-8 byte order of its name, JSON deflated and
|
|
3894
|
+
* everything else stored.
|
|
3864
3895
|
*
|
|
3865
|
-
* A tool's own `.<tool>/` folder is the tool's (spec §4):
|
|
3866
|
-
*
|
|
3896
|
+
* A tool's own `.<tool>/` folder is the tool's (spec §4): the reader returns
|
|
3897
|
+
* it like any file (a caller leaves files out with `skip`), and the writer
|
|
3898
|
+
* carries one it is handed, which makes the file a private copy (OW-7, OW-9).
|
|
3867
3899
|
*/
|
|
3868
|
-
/**
|
|
3900
|
+
/** The `mimetype` marker's exact bytes (spec §4.7, OW-6): ASCII, no newline. */
|
|
3901
|
+
declare const WORLD_FILE_MEDIA_TYPE = "application/vnd.onlyworlds.world+zip";
|
|
3902
|
+
/** The budget, uncompressed: the floor every reader accepts (spec §4.7, OW-15). */
|
|
3869
3903
|
declare const WORLD_FILE_MAX_BYTES: number;
|
|
3870
|
-
declare const WORLD_FILE_MAX_ENTRIES =
|
|
3871
|
-
type OwWorldFileReason = 'unsafe_path' | 'too_big' | 'too_many' | 'not_a_world' | 'more_than_one' | 'not_openable';
|
|
3904
|
+
declare const WORLD_FILE_MAX_ENTRIES = 500000;
|
|
3905
|
+
type OwWorldFileReason = 'unsafe_path' | 'too_big' | 'too_many' | 'not_a_world' | 'more_than_one' | 'not_openable' | 'duplicate' | 'collision' | 'link' | 'method' | 'name' | 'marker';
|
|
3872
3906
|
/** A world file was refused; nothing was returned. `reason` says why, for a tool to word its own message. */
|
|
3873
3907
|
declare class OwWorldFileError extends Error {
|
|
3874
3908
|
readonly reason: OwWorldFileReason;
|
|
@@ -3885,26 +3919,37 @@ interface OwReadWorldFile {
|
|
|
3885
3919
|
name: string;
|
|
3886
3920
|
/** `world.json`, parsed, without `api`. */
|
|
3887
3921
|
world: Record<string, unknown>;
|
|
3888
|
-
/** The world's files, relative to its folder, `world.json` included (without `api`),
|
|
3922
|
+
/** The world's files, relative to its folder, `world.json` included (without `api`), in code point order. */
|
|
3889
3923
|
files: OwWorldFileEntry[];
|
|
3890
3924
|
}
|
|
3891
3925
|
interface OwReadWorldFileOptions {
|
|
3892
3926
|
maxBytes?: number;
|
|
3893
3927
|
maxEntries?: number;
|
|
3894
|
-
/** Leave a file out of `files` (a path relative to the world's folder). It still
|
|
3928
|
+
/** Leave a file out of `files` (a path relative to the world's folder). It is still checked and counted. */
|
|
3895
3929
|
skip?: (path: string) => boolean;
|
|
3896
3930
|
}
|
|
3931
|
+
/**
|
|
3932
|
+
* Simple case folding (Unicode 15.1, CaseFolding.txt C and S), code point by
|
|
3933
|
+
* code point: what OW-18 applies after NFC. A code point the table does not
|
|
3934
|
+
* map folds to itself.
|
|
3935
|
+
*/
|
|
3936
|
+
declare function simpleCaseFold(text: string): string;
|
|
3897
3937
|
/**
|
|
3898
3938
|
* Read a world file. Throws `OwWorldFileError` saying why, having returned
|
|
3899
3939
|
* nothing; the caller writes only what this returns.
|
|
3900
3940
|
*/
|
|
3901
3941
|
declare function readWorldFile(bytes: Uint8Array, opts?: OwReadWorldFileOptions): OwReadWorldFile;
|
|
3902
3942
|
/**
|
|
3903
|
-
* Write a world file
|
|
3904
|
-
*
|
|
3905
|
-
*
|
|
3906
|
-
*
|
|
3907
|
-
*
|
|
3943
|
+
* Write a world file under one top-level folder `topDir`: the `mimetype`
|
|
3944
|
+
* marker first and stored, then `world.json` (without `api`) and the folder's
|
|
3945
|
+
* parts, in UTF-8 byte order, every entry stamped 1980-01-01 00:00, JSON
|
|
3946
|
+
* deflated and everything else stored (spec §4.7, OW-6, OW-7, OW-12, OW-13).
|
|
3947
|
+
* Any other file is left out. A tool's dot-folder in `files` travels, and
|
|
3948
|
+
* makes the result a private copy, not a portable `.owworld` (OW-7): leave it
|
|
3949
|
+
* out of `files` for a portable one. Refuses (`OwWorldFileError`) an unsafe
|
|
3950
|
+
* path or `topDir`, a duplicate path, a name some disk cannot hold, two names
|
|
3951
|
+
* that fold to one, and a world.json that is missing, will not parse (it may
|
|
3952
|
+
* hold a key, so it is never zipped raw) or has no `id`.
|
|
3908
3953
|
*/
|
|
3909
3954
|
declare function writeWorldFile(files: OwWorldFileEntry[], topDir: string): Uint8Array;
|
|
3910
3955
|
|
|
@@ -4717,4 +4762,4 @@ declare class TokenResource {
|
|
|
4717
4762
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
4718
4763
|
}
|
|
4719
4764
|
|
|
4720
|
-
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, 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, 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, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
|
4765
|
+
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, 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, 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
|
@@ -191,6 +191,13 @@ var OwApiError = class extends Error {
|
|
|
191
191
|
get isCharacterTaken() {
|
|
192
192
|
return this.status === 422 && this.code === "character_taken";
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* setMyCharacter, or the member PATCH's `character` (keel #421, PR 160): the picked Character is
|
|
196
|
+
* hidden (422 `character_hidden`, `param` character). Unhide it first, or pick another.
|
|
197
|
+
*/
|
|
198
|
+
get isCharacterHidden() {
|
|
199
|
+
return this.status === 422 && this.code === "character_hidden";
|
|
200
|
+
}
|
|
194
201
|
/**
|
|
195
202
|
* The `character` sent was refused (422 `invalid_request`, `param` character). On redeemJoin
|
|
196
203
|
* at a table (keel #422 PR 2): the pick is off the link's list, hidden or deleted since the
|
|
@@ -533,6 +540,19 @@ var OwV2Client = class {
|
|
|
533
540
|
* creates; null for the owner) and the member's Character. A world key's call; an account
|
|
534
541
|
* token is a TypeError before sending.
|
|
535
542
|
*/
|
|
543
|
+
/**
|
|
544
|
+
* PUT /me/character -- pick the roster Character that is you in this world, or none (keel PR 160,
|
|
545
|
+
* #533). A person member's write key with its PIN; answers GET /me's body. 422 for a hidden
|
|
546
|
+
* Character (`character_hidden`, `err.isCharacterHidden`), a guest's pick it did not make, or a
|
|
547
|
+
* taken Character (`character_taken`); 404 for one this key cannot see; 403 for an agent seat, a
|
|
548
|
+
* player, the owner (who sets it on the world page) or a read key. A guest's view resets once.
|
|
549
|
+
*/
|
|
550
|
+
async setMyCharacter(character) {
|
|
551
|
+
if (this.keyKind === "account") {
|
|
552
|
+
throw new TypeError("setMyCharacter needs a world key: an account token is not a member of one world");
|
|
553
|
+
}
|
|
554
|
+
return this.request("PUT", "/me/character", { write: true, body: { character } });
|
|
555
|
+
}
|
|
536
556
|
async me() {
|
|
537
557
|
if (this.keyKind === "account") {
|
|
538
558
|
throw new TypeError("me needs a world key: an account token is not a member of one world");
|
|
@@ -2514,17 +2534,25 @@ function getElementLabel(elementType) {
|
|
|
2514
2534
|
}
|
|
2515
2535
|
|
|
2516
2536
|
// src/world-file.ts
|
|
2517
|
-
import {
|
|
2537
|
+
import { Inflate, inflateSync, zipSync } from "fflate";
|
|
2518
2538
|
var WORLD_FILE = "world.json";
|
|
2519
|
-
var
|
|
2520
|
-
var
|
|
2539
|
+
var MARKER_NAME = "mimetype";
|
|
2540
|
+
var WORLD_FILE_MEDIA_TYPE = "application/vnd.onlyworlds.world+zip";
|
|
2541
|
+
var WORLD_FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
2542
|
+
var WORLD_FILE_MAX_ENTRIES = 5e5;
|
|
2521
2543
|
var MESSAGES = {
|
|
2522
2544
|
unsafe_path: "The world file holds a path that cannot be written safely.",
|
|
2523
|
-
too_big: "The world file is bigger than the budget allows.",
|
|
2545
|
+
too_big: "The world file is bigger than the budget allows, or a file in it is bigger than it says.",
|
|
2524
2546
|
too_many: "The world file holds more files than the budget allows.",
|
|
2525
2547
|
not_a_world: "The file is not a world: it has no readable world.json with an id.",
|
|
2526
|
-
more_than_one: "The file holds more than one world
|
|
2527
|
-
not_openable: "The file is not a zip that can be read."
|
|
2548
|
+
more_than_one: "The file holds more than one world.",
|
|
2549
|
+
not_openable: "The file is not a zip that can be read.",
|
|
2550
|
+
duplicate: "The world file holds two entries with the same name.",
|
|
2551
|
+
collision: "The world file holds two names that differ only in case or in how an accent is written.",
|
|
2552
|
+
link: "The world file holds a link or a special file.",
|
|
2553
|
+
method: "The world file holds an encrypted entry, or one compressed other than stored or deflate.",
|
|
2554
|
+
name: "The world file holds a name that some disk cannot hold.",
|
|
2555
|
+
marker: "The world file's mimetype entry is not the OnlyWorlds media type."
|
|
2528
2556
|
};
|
|
2529
2557
|
var OwWorldFileError = class extends Error {
|
|
2530
2558
|
constructor(reason, message = MESSAGES[reason]) {
|
|
@@ -2533,13 +2561,228 @@ var OwWorldFileError = class extends Error {
|
|
|
2533
2561
|
this.reason = reason;
|
|
2534
2562
|
}
|
|
2535
2563
|
};
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2564
|
+
var refuse = (reason, message) => {
|
|
2565
|
+
throw new OwWorldFileError(reason, message);
|
|
2566
|
+
};
|
|
2567
|
+
function pathFault(name) {
|
|
2568
|
+
if (name.includes("\\")) return "holds a backslash";
|
|
2569
|
+
if (name.startsWith("/")) return "is absolute";
|
|
2570
|
+
if (/^[A-Za-z]:/.test(name)) return "names a drive";
|
|
2571
|
+
if (name.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) return "has an empty, . or .. segment";
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
2575
|
+
"CON",
|
|
2576
|
+
"PRN",
|
|
2577
|
+
"AUX",
|
|
2578
|
+
"NUL",
|
|
2579
|
+
...[1, 2, 3, 4, 5, 6, 7, 8, 9].flatMap((i) => [`COM${i}`, `LPT${i}`])
|
|
2580
|
+
]);
|
|
2581
|
+
var BAD_CHAR = /[<>:"|?*\p{Cc}]/u;
|
|
2582
|
+
function nameFault(name) {
|
|
2583
|
+
for (const seg of name.split("/")) {
|
|
2584
|
+
if (BAD_CHAR.test(seg)) return `segment ${JSON.stringify(seg)} holds a reserved or control character`;
|
|
2585
|
+
if (seg.endsWith(".") || seg.endsWith(" ")) return `segment ${JSON.stringify(seg)} ends in a dot or a space`;
|
|
2586
|
+
if (RESERVED.has(seg.split(".")[0].toUpperCase())) return `segment ${JSON.stringify(seg)} is a Windows device name`;
|
|
2587
|
+
}
|
|
2588
|
+
return null;
|
|
2539
2589
|
}
|
|
2540
2590
|
function isJunk(path) {
|
|
2541
2591
|
return path.split("/").some((seg) => seg === "__MACOSX" || seg === ".DS_Store" || seg === "Thumbs.db");
|
|
2542
2592
|
}
|
|
2593
|
+
var FOLD_RUNS = "1t.q..w,51...lj,5c.n..w,60.7..w,74.o.2.1,8i.3.2.1,8p.8.2.1,96.n.2.1,ag...-3d,ah.3.2.1,an...-7g,ap...5u,aq.2.2.1,au...5q,av...1,ax.2..5p,az...1,b2...27,b3...5m,b4...5n,b5...1,b7...5p,b8...5r,ba...5v,bb...5t,bc...1,bg...5v,bh...5x,bj...5y,bk.3.2.1,bq...62,br...1,bt...62,bw...1,by...62,bz...1,c1.2..61,c3.2.2.1,c7...63,c8...1,cc...1,ck...2,cl...1,cn...2,co...1,cq...2,cr.9.2.1,da.9.2.1,dt...2,du.2.2.1,dy...-2p,dz...-1k,e0.k.2.1,f4...-3m,f6.9.2.1,fu...8bv,fv...1,fx...-4j,fy...8bs,g1...1,g3...-5f,g4...1x,g5...1z,g6.5.2.1,n9...38,og.2.2.1,om...1,ov...38,p2...12,p4.3..11,p8...1s,pa.2..1r,pd.h..w,pv.9..w,qq...1,r3...8,r4...-u,r5...-p,r9...-f,ra...-m,rc.c.2.1,s0...-1i,s1...-1c,s4...-1o,s5...-1s,s7...1,s9...-7,sa...1,sd.3..-3m,sg.g..28,sw.w..w,v4.h.2.1,wa.r.2.1,xs...f,xt.7.2.1,y8.1c.2.1,10x.12..1c,3a8.12..5ls,3bb...5ls,3bh...5ls,3y0.6..-8,5mo...-4su,5mp...-4st,5mq...-4sk,5mr.2..-4si,5mt...-4sj,5mu...-4sc,5mv...-4ro,5mw...r7n,5n4.17..-2bk,5od.3..-2bk,5xc.23.2.1,61n...-1m,61q...-5vj,61s.1c.2.1,64o.8..-8,654.6..-8,65k.8..-8,660.8..-8,66g.6..-8,66x.4.2.-8,67c.8..-8,688.8..-8,68o.8..-8,694.8..-8,69k.2..-8,69m.2..-22,69o...-9,69q...-5j9,6a0.4..-2e,6a4...-9,6ab...-5kz,6ag.2..-8,6ai.2..-2s,6ar...-5kj,6aw.2..-8,6ay.2..-34,6b0...-7,6bc.2..-3k,6be.2..-3i,6bg...-9,6jq...-5st,6ju...-6gv,6jv...-6di,6k2...s,6lc.g..g,6mb...1,792.q..q,8ow.1c..1c,8rk...1,8rm...-8af,8rn...-2xy,8ro...-89z,8rr.3.2.1,8rx...-8bg,8ry...-8al,8rz...-8bj,8s0...-8bi,8s2...1,8s5...1,8se.2..-8cf,8sg.1e.2.1,8vf.2.2.1,8vm...1,wu8.n.2.1,ww0.e.2.1,x0i.7.2.1,x0y.v.2.1,x2x.2.2.1,x31...-r9g,x32.5.2.1,x3f...1,x3h...-wmg,x3k.2.2.1,x3q.a.2.1,x4a...-wn8,x4b...-wnj,x4c...-wnf,x4d...-wn5,x4e...-wn8,x4g...-wlu,x4h...-wmi,x4i...-wlx,x4j...ps,x4k.8.2.1,x50...-1c,x51...-wn7,x52...-raw,x53.2.2.1,x5c...1,x5i.2.2.1,x6d...1,xv4.28..-tzk,1dl1...1,1ee9.q..w,1fcw.14..14,1fhs.10..14,1fn4.b..13,1fng.f..13,1fnw.7..13,1fo4.2..13,1h1c.1f..1s,1jfk.w..w,20cg.w..w,2olc.y..y";
|
|
2594
|
+
var foldTable = null;
|
|
2595
|
+
function simpleCaseFold(text) {
|
|
2596
|
+
if (!foldTable) {
|
|
2597
|
+
foldTable = /* @__PURE__ */ new Map();
|
|
2598
|
+
for (const run of FOLD_RUNS.split(",")) {
|
|
2599
|
+
const [start, count, stride, delta] = run.split(".");
|
|
2600
|
+
const s = parseInt(start, 36);
|
|
2601
|
+
const n = count ? parseInt(count, 36) : 1;
|
|
2602
|
+
const step = stride ? parseInt(stride, 36) : 1;
|
|
2603
|
+
const d = parseInt(delta, 36);
|
|
2604
|
+
for (let k = 0; k < n; k++) foldTable.set(s + k * step, s + k * step + d);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
let out = "";
|
|
2608
|
+
for (const c of text) {
|
|
2609
|
+
const to = foldTable.get(c.codePointAt(0));
|
|
2610
|
+
out += to === void 0 ? c : String.fromCodePoint(to);
|
|
2611
|
+
}
|
|
2612
|
+
return out;
|
|
2613
|
+
}
|
|
2614
|
+
function foldName(name) {
|
|
2615
|
+
return simpleCaseFold(name.normalize("NFC"));
|
|
2616
|
+
}
|
|
2617
|
+
function byCodePoint(a, b) {
|
|
2618
|
+
const n = Math.min(a.length, b.length);
|
|
2619
|
+
for (let i = 0; i < n; i++) {
|
|
2620
|
+
const x = a.codePointAt(i);
|
|
2621
|
+
const y = b.codePointAt(i);
|
|
2622
|
+
if (x !== y) return x - y;
|
|
2623
|
+
if (x > 65535) i++;
|
|
2624
|
+
}
|
|
2625
|
+
return a.length - b.length;
|
|
2626
|
+
}
|
|
2627
|
+
var u16 = (b, o) => b[o] | b[o + 1] << 8;
|
|
2628
|
+
var u32 = (b, o) => (b[o] | b[o + 1] << 8 | b[o + 2] << 16) + b[o + 3] * 16777216;
|
|
2629
|
+
var u64 = (b, o) => u32(b, o) + u32(b, o + 4) * 4294967296;
|
|
2630
|
+
var SIG_LOCAL = 67324752;
|
|
2631
|
+
var SIG_CENTRAL = 33639248;
|
|
2632
|
+
var SIG_END = 101010256;
|
|
2633
|
+
var SIG_END64 = 101075792;
|
|
2634
|
+
var SIG_LOCATOR64 = 117853008;
|
|
2635
|
+
var MAX32 = 4294967295;
|
|
2636
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
2637
|
+
var latin1 = (b) => {
|
|
2638
|
+
let s = "";
|
|
2639
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
2640
|
+
return s;
|
|
2641
|
+
};
|
|
2642
|
+
function centralDirectory(bytes) {
|
|
2643
|
+
const len = bytes.length;
|
|
2644
|
+
let end = -1;
|
|
2645
|
+
for (let i = len - 22; i >= Math.max(0, len - 22 - 65535); i--) {
|
|
2646
|
+
if (u32(bytes, i) === SIG_END) {
|
|
2647
|
+
end = i;
|
|
2648
|
+
break;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (end < 0) refuse("not_openable", "The file is not a zip: it has no end of central directory.");
|
|
2652
|
+
const thisDisk = u16(bytes, end + 4);
|
|
2653
|
+
const cdDisk = u16(bytes, end + 6);
|
|
2654
|
+
if (!(thisDisk === 0 && cdDisk === 0 || thisDisk === 65535 && cdDisk === 65535)) {
|
|
2655
|
+
refuse("not_openable", `The zip spans disks (its end record says disk ${thisDisk}, the directory on disk ${cdDisk}).`);
|
|
2656
|
+
}
|
|
2657
|
+
let cdSize = u32(bytes, end + 12);
|
|
2658
|
+
let cdOffset = u32(bytes, end + 16);
|
|
2659
|
+
const loc = end - 20;
|
|
2660
|
+
if (loc >= 0 && u32(bytes, loc) === SIG_LOCATOR64) {
|
|
2661
|
+
if (u32(bytes, loc + 4) !== 0 || u32(bytes, loc + 16) > 1) refuse("not_openable", "The zip spans disks (zip64).");
|
|
2662
|
+
const rec = u64(bytes, loc + 8);
|
|
2663
|
+
if (rec + 56 > len || u32(bytes, rec) !== SIG_END64) refuse("not_openable", "The zip64 end record is missing.");
|
|
2664
|
+
if (u32(bytes, rec + 16) !== 0 || u32(bytes, rec + 20) !== 0) refuse("not_openable", "The zip spans disks (zip64).");
|
|
2665
|
+
cdSize = u64(bytes, rec + 40);
|
|
2666
|
+
cdOffset = u64(bytes, rec + 48);
|
|
2667
|
+
}
|
|
2668
|
+
const cdEnd = cdOffset + cdSize;
|
|
2669
|
+
if (cdEnd > len) refuse("not_openable", "The central directory lies past the end of the file.");
|
|
2670
|
+
const out = [];
|
|
2671
|
+
let p = cdOffset;
|
|
2672
|
+
while (p < cdEnd) {
|
|
2673
|
+
if (p + 46 > cdEnd || u32(bytes, p) !== SIG_CENTRAL) refuse("not_openable", "The central directory is broken.");
|
|
2674
|
+
const flags = u16(bytes, p + 8);
|
|
2675
|
+
const nlen = u16(bytes, p + 28);
|
|
2676
|
+
const elen = u16(bytes, p + 30);
|
|
2677
|
+
const clen = u16(bytes, p + 32);
|
|
2678
|
+
const nameBytes = bytes.subarray(p + 46, p + 46 + nlen);
|
|
2679
|
+
const extra = bytes.subarray(p + 46 + nlen, p + 46 + nlen + elen);
|
|
2680
|
+
let usize = u32(bytes, p + 24);
|
|
2681
|
+
let csize = u32(bytes, p + 20);
|
|
2682
|
+
let offset = u32(bytes, p + 42);
|
|
2683
|
+
if (usize === MAX32 || csize === MAX32 || offset === MAX32) {
|
|
2684
|
+
for (let q = 0; q + 4 <= extra.length; q += 4 + u16(extra, q + 2)) {
|
|
2685
|
+
if (u16(extra, q) !== 1) continue;
|
|
2686
|
+
let r = q + 4;
|
|
2687
|
+
const stop = r + u16(extra, q + 2);
|
|
2688
|
+
const next = () => {
|
|
2689
|
+
if (r + 8 > stop) refuse("not_openable", "A zip64 extra field is short.");
|
|
2690
|
+
const v = u64(extra, r);
|
|
2691
|
+
r += 8;
|
|
2692
|
+
return v;
|
|
2693
|
+
};
|
|
2694
|
+
if (usize === MAX32) usize = next();
|
|
2695
|
+
if (csize === MAX32) csize = next();
|
|
2696
|
+
if (offset === MAX32) offset = next();
|
|
2697
|
+
break;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
const nonAscii = nameBytes.some((x) => x > 127);
|
|
2701
|
+
let name;
|
|
2702
|
+
let badUtf8 = false;
|
|
2703
|
+
if (flags & 2048) {
|
|
2704
|
+
try {
|
|
2705
|
+
name = utf8.decode(nameBytes);
|
|
2706
|
+
} catch {
|
|
2707
|
+
name = latin1(nameBytes);
|
|
2708
|
+
badUtf8 = true;
|
|
2709
|
+
}
|
|
2710
|
+
} else {
|
|
2711
|
+
name = latin1(nameBytes);
|
|
2712
|
+
}
|
|
2713
|
+
out.push({
|
|
2714
|
+
name,
|
|
2715
|
+
flags,
|
|
2716
|
+
method: u16(bytes, p + 10),
|
|
2717
|
+
crc: u32(bytes, p + 16),
|
|
2718
|
+
csize,
|
|
2719
|
+
usize,
|
|
2720
|
+
offset,
|
|
2721
|
+
external: u32(bytes, p + 38),
|
|
2722
|
+
nonAscii,
|
|
2723
|
+
badUtf8
|
|
2724
|
+
});
|
|
2725
|
+
p += 46 + nlen + elen + clen;
|
|
2726
|
+
}
|
|
2727
|
+
if (p !== cdEnd) refuse("not_openable", "The central directory is broken.");
|
|
2728
|
+
return out;
|
|
2729
|
+
}
|
|
2730
|
+
var crcTable = null;
|
|
2731
|
+
function crc32(data) {
|
|
2732
|
+
if (!crcTable) {
|
|
2733
|
+
crcTable = new Int32Array(256);
|
|
2734
|
+
for (let n = 0; n < 256; n++) {
|
|
2735
|
+
let c2 = n;
|
|
2736
|
+
for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
|
|
2737
|
+
crcTable[n] = c2;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
let c = -1;
|
|
2741
|
+
for (let i = 0; i < data.length; i++) c = crcTable[(c ^ data[i]) & 255] ^ c >>> 8;
|
|
2742
|
+
return (c ^ -1) >>> 0;
|
|
2743
|
+
}
|
|
2744
|
+
var CHUNK = 64 * 1024;
|
|
2745
|
+
function inflateEntry(bytes, e) {
|
|
2746
|
+
const off = e.offset;
|
|
2747
|
+
if (off + 30 > bytes.length || u32(bytes, off) !== SIG_LOCAL) refuse("not_openable", `${e.name}: no local header at its offset.`);
|
|
2748
|
+
const start = off + 30 + u16(bytes, off + 26) + u16(bytes, off + 28);
|
|
2749
|
+
const payload = bytes.subarray(start, start + e.csize);
|
|
2750
|
+
let data;
|
|
2751
|
+
if (e.method === 0) {
|
|
2752
|
+
data = payload;
|
|
2753
|
+
} else if (payload.length <= CHUNK) {
|
|
2754
|
+
try {
|
|
2755
|
+
data = inflateSync(payload);
|
|
2756
|
+
} catch {
|
|
2757
|
+
return refuse("not_openable", `${e.name}: its deflate stream is broken.`);
|
|
2758
|
+
}
|
|
2759
|
+
} else {
|
|
2760
|
+
const out = new Uint8Array(e.usize);
|
|
2761
|
+
let n = 0;
|
|
2762
|
+
let over = false;
|
|
2763
|
+
const inf = new Inflate((chunk) => {
|
|
2764
|
+
if (n + chunk.length > e.usize) {
|
|
2765
|
+
over = true;
|
|
2766
|
+
throw new Error("past its size");
|
|
2767
|
+
}
|
|
2768
|
+
out.set(chunk, n);
|
|
2769
|
+
n += chunk.length;
|
|
2770
|
+
});
|
|
2771
|
+
try {
|
|
2772
|
+
for (let i = 0; i < payload.length; i += CHUNK) {
|
|
2773
|
+
const stop = Math.min(i + CHUNK, payload.length);
|
|
2774
|
+
inf.push(payload.subarray(i, stop), stop === payload.length);
|
|
2775
|
+
}
|
|
2776
|
+
} catch {
|
|
2777
|
+
if (!over) refuse("not_openable", `${e.name}: its deflate stream is broken.`);
|
|
2778
|
+
}
|
|
2779
|
+
data = over ? new Uint8Array(e.usize + 1) : out.subarray(0, n);
|
|
2780
|
+
}
|
|
2781
|
+
if (data.length > e.usize) refuse("too_big", `${e.name} holds more than the ${e.usize} bytes it declares.`);
|
|
2782
|
+
if (data.length !== e.usize) refuse("not_openable", `${e.name} holds ${data.length} bytes and declares ${e.usize}.`);
|
|
2783
|
+
if (crc32(data) !== e.crc) refuse("not_openable", `${e.name} fails its CRC.`);
|
|
2784
|
+
return e.method === 0 ? data.slice() : data;
|
|
2785
|
+
}
|
|
2543
2786
|
function parseWorld(data) {
|
|
2544
2787
|
let meta;
|
|
2545
2788
|
try {
|
|
@@ -2557,93 +2800,157 @@ function withoutLink(data, meta) {
|
|
|
2557
2800
|
const { api: _link, ...rest } = meta;
|
|
2558
2801
|
return { data: new TextEncoder().encode(JSON.stringify(rest, null, 2) + "\n"), world: rest };
|
|
2559
2802
|
}
|
|
2560
|
-
function
|
|
2561
|
-
const
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
refusal = "unsafe_path";
|
|
2574
|
-
return false;
|
|
2575
|
-
}
|
|
2576
|
-
if (isJunk(file.name)) return false;
|
|
2577
|
-
count++;
|
|
2578
|
-
declared += file.originalSize;
|
|
2579
|
-
if (count > maxEntries) {
|
|
2580
|
-
refusal = "too_many";
|
|
2581
|
-
return false;
|
|
2582
|
-
}
|
|
2583
|
-
if (declared > maxBytes) {
|
|
2584
|
-
refusal = "too_big";
|
|
2585
|
-
return false;
|
|
2586
|
-
}
|
|
2587
|
-
return true;
|
|
2803
|
+
function checkCollisions(names) {
|
|
2804
|
+
const byFold = /* @__PURE__ */ new Map();
|
|
2805
|
+
const segFold = /* @__PURE__ */ new Map();
|
|
2806
|
+
const done = /* @__PURE__ */ new Set();
|
|
2807
|
+
for (const raw of names) {
|
|
2808
|
+
const segs = (raw.endsWith("/") ? raw.slice(0, -1) : raw).split("/");
|
|
2809
|
+
let prefix = "";
|
|
2810
|
+
let folded = "";
|
|
2811
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2812
|
+
let f = segFold.get(segs[i]);
|
|
2813
|
+
if (f === void 0) {
|
|
2814
|
+
f = foldName(segs[i]);
|
|
2815
|
+
segFold.set(segs[i], f);
|
|
2588
2816
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2817
|
+
prefix = i ? `${prefix}/${segs[i]}` : segs[i];
|
|
2818
|
+
folded = i ? `${folded}/${f}` : f;
|
|
2819
|
+
if (done.has(prefix)) continue;
|
|
2820
|
+
done.add(prefix);
|
|
2821
|
+
const other = byFold.get(folded);
|
|
2822
|
+
if (other !== void 0) {
|
|
2823
|
+
refuse("collision", `The names ${JSON.stringify(other)} and ${JSON.stringify(prefix)} are one name after NFC and case folding.`);
|
|
2824
|
+
}
|
|
2825
|
+
byFold.set(folded, prefix);
|
|
2826
|
+
}
|
|
2592
2827
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
const
|
|
2597
|
-
for (const n of names) {
|
|
2828
|
+
}
|
|
2829
|
+
function checkFileAndFolder(files) {
|
|
2830
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2831
|
+
for (const n of files) {
|
|
2598
2832
|
const segs = n.split("/");
|
|
2599
|
-
|
|
2600
|
-
|
|
2833
|
+
let prefix = "";
|
|
2834
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
2835
|
+
prefix = i ? `${prefix}/${segs[i]}` : segs[i];
|
|
2836
|
+
if (seen.has(prefix)) continue;
|
|
2837
|
+
seen.add(prefix);
|
|
2838
|
+
if (files.has(prefix)) refuse("unsafe_path", `${JSON.stringify(prefix)} is a file and a folder.`);
|
|
2601
2839
|
}
|
|
2602
2840
|
}
|
|
2841
|
+
}
|
|
2842
|
+
function readWorldFile(bytes, opts = {}) {
|
|
2843
|
+
const maxBytes = opts.maxBytes ?? WORLD_FILE_MAX_BYTES;
|
|
2844
|
+
const maxEntries = opts.maxEntries ?? WORLD_FILE_MAX_ENTRIES;
|
|
2845
|
+
const entries = centralDirectory(bytes);
|
|
2846
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2847
|
+
const files = [];
|
|
2848
|
+
let declared = 0;
|
|
2849
|
+
for (const e of entries) {
|
|
2850
|
+
const name2 = e.name;
|
|
2851
|
+
const isDir = name2.endsWith("/");
|
|
2852
|
+
const bare = isDir ? name2.slice(0, -1) : name2;
|
|
2853
|
+
const quoted = JSON.stringify(name2);
|
|
2854
|
+
const fault = pathFault(bare);
|
|
2855
|
+
if (fault) refuse("unsafe_path", `The entry ${quoted} ${fault}.`);
|
|
2856
|
+
const kind = e.external >>> 16 & 61440;
|
|
2857
|
+
if (kind !== 0 && kind !== 32768 && kind !== 16384) refuse("link", `The entry ${quoted} is a link or a special file.`);
|
|
2858
|
+
if (e.flags & 65) refuse("method", `The entry ${quoted} is encrypted.`);
|
|
2859
|
+
if (e.method !== 0 && e.method !== 8) refuse("method", `The entry ${quoted} uses method ${e.method} (only stored and deflate).`);
|
|
2860
|
+
if (e.badUtf8) refuse("name", `The entry ${quoted} is flagged UTF-8 and is not.`);
|
|
2861
|
+
if (e.nonAscii && !(e.flags & 2048)) refuse("name", `The entry ${quoted} is not ASCII and lacks the UTF-8 flag.`);
|
|
2862
|
+
const bad = nameFault(bare);
|
|
2863
|
+
if (bad) refuse("name", `The entry ${quoted}: ${bad}.`);
|
|
2864
|
+
if (seen.has(name2)) refuse("duplicate", `The entry ${quoted} appears twice.`);
|
|
2865
|
+
seen.add(name2);
|
|
2866
|
+
if (isDir) continue;
|
|
2867
|
+
files.push(e);
|
|
2868
|
+
if (files.length > maxEntries) refuse("too_many", `The world file holds more than ${maxEntries} files.`);
|
|
2869
|
+
declared += e.usize;
|
|
2870
|
+
if (declared > maxBytes) refuse("too_big", `The world file declares more than ${maxBytes} bytes.`);
|
|
2871
|
+
}
|
|
2872
|
+
checkCollisions(seen);
|
|
2873
|
+
const names = new Set(files.map((e) => e.name));
|
|
2874
|
+
checkFileAndFolder(names);
|
|
2875
|
+
const marker = files.find((e) => e.name === MARKER_NAME);
|
|
2876
|
+
if (marker && new TextDecoder().decode(inflateEntry(bytes, marker)) !== WORLD_FILE_MEDIA_TYPE) {
|
|
2877
|
+
refuse("marker", "The mimetype entry holds other bytes than the OnlyWorlds media type.");
|
|
2878
|
+
}
|
|
2879
|
+
const candidates = [...names].filter((n) => n !== MARKER_NAME && !isJunk(n));
|
|
2603
2880
|
let prefix;
|
|
2604
|
-
if (
|
|
2881
|
+
if (candidates.includes(WORLD_FILE)) {
|
|
2605
2882
|
prefix = "";
|
|
2606
2883
|
} else {
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
if (
|
|
2610
|
-
|
|
2611
|
-
|
|
2884
|
+
const roots = new Set(candidates.filter((n) => n.split("/").length === 2 && n.endsWith(`/${WORLD_FILE}`) && !n.startsWith(".")).map((n) => n.slice(0, -WORLD_FILE.length)));
|
|
2885
|
+
if (roots.size === 0) refuse("not_a_world", "There is no world.json at the root or in one top-level folder.");
|
|
2886
|
+
if (roots.size > 1) refuse("more_than_one", `The file holds worlds in ${[...roots].join(", ")}.`);
|
|
2887
|
+
prefix = [...roots][0];
|
|
2888
|
+
}
|
|
2889
|
+
const inside = new Set(candidates.filter((n) => n.startsWith(prefix)));
|
|
2890
|
+
for (const n of inside) {
|
|
2891
|
+
const rel = n.slice(prefix.length);
|
|
2892
|
+
if (rel !== WORLD_FILE && rel.endsWith(`/${WORLD_FILE}`) && !rel.startsWith("records/")) {
|
|
2893
|
+
refuse("more_than_one", `A second world.json sits inside the world: ${n}.`);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
const content = /* @__PURE__ */ new Map();
|
|
2897
|
+
for (const e of files) {
|
|
2898
|
+
const data = inflateEntry(bytes, e);
|
|
2899
|
+
if (inside.has(e.name)) content.set(e.name, data);
|
|
2612
2900
|
}
|
|
2613
2901
|
const worldName = `${prefix}${WORLD_FILE}`;
|
|
2614
|
-
|
|
2615
|
-
const
|
|
2616
|
-
const
|
|
2617
|
-
for (const n of names) {
|
|
2902
|
+
const parsed = withoutLink(content.get(worldName), parseWorld(content.get(worldName)));
|
|
2903
|
+
const out = [];
|
|
2904
|
+
for (const [n, data] of content) {
|
|
2618
2905
|
const path = n.slice(prefix.length);
|
|
2619
2906
|
if (opts.skip?.(path)) continue;
|
|
2620
|
-
|
|
2907
|
+
out.push({ path, data: n === worldName ? parsed.data : data });
|
|
2621
2908
|
}
|
|
2622
|
-
|
|
2909
|
+
out.sort((a, b) => byCodePoint(a.path, b.path));
|
|
2623
2910
|
const name = parsed.world.name;
|
|
2624
2911
|
return {
|
|
2625
2912
|
worldId: parsed.world.id,
|
|
2626
2913
|
name: typeof name === "string" ? name : "",
|
|
2627
2914
|
world: parsed.world,
|
|
2628
|
-
files
|
|
2915
|
+
files: out
|
|
2629
2916
|
};
|
|
2630
2917
|
}
|
|
2918
|
+
var PARTS = /* @__PURE__ */ new Set(["elements", "spatial", "media", "records"]);
|
|
2919
|
+
function carried(path) {
|
|
2920
|
+
if (path === WORLD_FILE) return true;
|
|
2921
|
+
const segs = path.split("/");
|
|
2922
|
+
if (segs.length < 2) return false;
|
|
2923
|
+
if (segs[0].startsWith(".")) return true;
|
|
2924
|
+
if (!PARTS.has(segs[0])) return false;
|
|
2925
|
+
if (segs[0] === "records") return true;
|
|
2926
|
+
return !segs.some((s) => s.startsWith(".")) && !path.endsWith(".meta");
|
|
2927
|
+
}
|
|
2928
|
+
var STAMP = new Date(1980, 0, 1, 0, 0, 0);
|
|
2631
2929
|
function writeWorldFile(files, topDir) {
|
|
2632
|
-
if (
|
|
2633
|
-
const
|
|
2634
|
-
let sawWorld = false;
|
|
2930
|
+
if (pathFault(topDir) || topDir.includes("/")) throw new OwWorldFileError("unsafe_path");
|
|
2931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2635
2932
|
for (const f of files) {
|
|
2636
|
-
if (
|
|
2637
|
-
|
|
2638
|
-
|
|
2933
|
+
if (pathFault(f.path)) throw new OwWorldFileError("unsafe_path");
|
|
2934
|
+
if (seen.has(f.path)) throw new OwWorldFileError("unsafe_path", `The path ${f.path} is listed twice.`);
|
|
2935
|
+
seen.add(f.path);
|
|
2936
|
+
}
|
|
2937
|
+
const kept = files.filter((f) => carried(f.path));
|
|
2938
|
+
if (!kept.some((f) => f.path === WORLD_FILE)) throw new OwWorldFileError("not_a_world");
|
|
2939
|
+
const keys = kept.map((f) => `${topDir}/${f.path}`);
|
|
2940
|
+
for (const key of keys) {
|
|
2941
|
+
const bad = nameFault(key);
|
|
2942
|
+
if (bad) throw new OwWorldFileError("name", `The path ${key}: ${bad}.`);
|
|
2943
|
+
}
|
|
2944
|
+
checkCollisions([MARKER_NAME, ...keys]);
|
|
2945
|
+
checkFileAndFolder(/* @__PURE__ */ new Set([MARKER_NAME, ...keys]));
|
|
2946
|
+
const input = {
|
|
2947
|
+
[MARKER_NAME]: [new TextEncoder().encode(WORLD_FILE_MEDIA_TYPE), { level: 0, mtime: STAMP }]
|
|
2948
|
+
};
|
|
2949
|
+
for (const f of [...kept].sort((a, b) => byCodePoint(a.path, b.path))) {
|
|
2639
2950
|
let data = f.data;
|
|
2640
|
-
if (f.path === WORLD_FILE)
|
|
2641
|
-
|
|
2642
|
-
sawWorld = true;
|
|
2643
|
-
}
|
|
2644
|
-
input[key] = [data, { level: /\.json$/i.test(f.path) ? 6 : 0 }];
|
|
2951
|
+
if (f.path === WORLD_FILE) data = withoutLink(data, parseWorld(data)).data;
|
|
2952
|
+
input[`${topDir}/${f.path}`] = [data, { level: /\.json$/i.test(f.path) ? 6 : 0, mtime: STAMP }];
|
|
2645
2953
|
}
|
|
2646
|
-
if (!sawWorld) throw new OwWorldFileError("not_a_world");
|
|
2647
2954
|
return zipSync(input);
|
|
2648
2955
|
}
|
|
2649
2956
|
|
|
@@ -3885,6 +4192,7 @@ export {
|
|
|
3885
4192
|
TokenResource,
|
|
3886
4193
|
WORLD_FILE_MAX_BYTES,
|
|
3887
4194
|
WORLD_FILE_MAX_ENTRIES,
|
|
4195
|
+
WORLD_FILE_MEDIA_TYPE,
|
|
3888
4196
|
absYearForEraYear,
|
|
3889
4197
|
absoluteYear,
|
|
3890
4198
|
approxYearLength,
|
|
@@ -3928,6 +4236,7 @@ export {
|
|
|
3928
4236
|
renderElement,
|
|
3929
4237
|
resolveCalendarLens,
|
|
3930
4238
|
resolveEraDomains,
|
|
4239
|
+
simpleCaseFold,
|
|
3931
4240
|
validateCalendarConfig,
|
|
3932
4241
|
weekdayForStep,
|
|
3933
4242
|
worldCalendars,
|