@onlyworlds/sdk 3.1.0 → 4.0.0-alpha.1

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/dist/index.js CHANGED
@@ -1,117 +1,374 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
1
+ // src/v2/errors.ts
2
+ var OwApiError = class extends Error {
3
+ constructor(status, code, message, docUrl, detail, type = null, param = null) {
4
+ super(message);
5
+ this.name = "OwApiError";
6
+ this.status = status;
7
+ this.code = code;
8
+ this.type = type;
9
+ this.param = param;
10
+ this.docUrl = docUrl;
11
+ this.detail = detail;
12
+ }
13
+ get isAuthError() {
14
+ return this.code === "invalid_credentials" || this.code === "key_revoked" || this.code === "world_gone";
15
+ }
16
+ /** 422s/400s name the offending param/field -- typos error loudly platform-wide. */
17
+ get isValidationError() {
18
+ return this.status === 422 || this.status === 400;
19
+ }
20
+ /** Same Idempotency-Key replayed with a different payload. */
21
+ get isIdempotencyConflict() {
22
+ return this.status === 409;
23
+ }
9
24
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
25
+ var OwNetworkError = class extends Error {
26
+ constructor(message, cause) {
27
+ super(message);
28
+ this.name = "OwNetworkError";
29
+ this.cause2 = cause;
15
30
  }
16
- return to;
17
31
  };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+ function parseErrorEnvelope(status, body) {
33
+ const env = body && typeof body === "object" ? body : {};
34
+ const nested = typeof env.error === "object" && env.error !== null ? env.error : void 0;
35
+ const code = env.code ?? nested?.code ?? (typeof env.error === "string" ? env.error : null) ?? null;
36
+ const type = env.type ?? nested?.type ?? null;
37
+ const param = env.param ?? nested?.param ?? null;
38
+ const docUrl = env.doc_url ?? nested?.doc_url ?? null;
39
+ const message = env.message ?? nested?.message ?? (typeof env.detail === "string" ? env.detail : void 0) ?? `OnlyWorlds API error ${status}${code ? ` (${code})` : ""}`;
40
+ return new OwApiError(status, code, message, docUrl, body, type, param);
41
+ }
42
+ async function errorFromResponse(res) {
43
+ let body = null;
44
+ let text = "";
45
+ try {
46
+ text = await res.text();
47
+ body = text ? JSON.parse(text) : null;
48
+ } catch {
49
+ body = text || null;
50
+ }
51
+ return parseErrorEnvelope(res.status, body);
52
+ }
19
53
 
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- ELEMENT_FAMILIES: () => ELEMENT_FAMILIES,
24
- ELEMENT_ICONS: () => ELEMENT_ICONS,
25
- ELEMENT_LABELS: () => ELEMENT_LABELS,
26
- ELEMENT_SECTIONS: () => ELEMENT_SECTIONS,
27
- ELEMENT_TYPES: () => ELEMENT_TYPES,
28
- ElementType: () => ElementType,
29
- FAMILY_COLORS: () => FAMILY_COLORS,
30
- FAMILY_ORDER: () => FAMILY_ORDER,
31
- FIELD_SCHEMA: () => FIELD_SCHEMA,
32
- GameTier: () => GameTier,
33
- ONLYWORLDS_VERSION: () => ONLYWORLDS_VERSION,
34
- OnlyWorldsClient: () => OnlyWorldsClient,
35
- OwApiError: () => OwApiError,
36
- OwNetworkError: () => OwNetworkError,
37
- OwV2Client: () => OwV2Client,
38
- SPATIAL_TYPES: () => SPATIAL_TYPES,
39
- createAnyElementId: () => createAnyElementId,
40
- createElementId: () => createElementId,
41
- createElementIds: () => createElementIds,
42
- detectKeyKind: () => detectKeyKind,
43
- elementColor: () => elementColor,
44
- errorFromResponse: () => errorFromResponse,
45
- familyOf: () => familyOf,
46
- getElementIcon: () => getElementIcon,
47
- getElementLabel: () => getElementLabel,
48
- getElementSections: () => getElementSections,
49
- isDemoKey: () => isDemoKey,
50
- kindCanWrite: () => kindCanWrite,
51
- parseEnvelope: () => parseEnvelope,
52
- pinExpectation: () => pinExpectation
53
- });
54
- module.exports = __toCommonJS(index_exports);
54
+ // src/v2/keys.ts
55
+ var LEGACY_RE = /^[0-9]{10}$/;
56
+ function isDemoKey(key) {
57
+ return LEGACY_RE.test(key) && key >= "0000000000" && key <= "0000000009";
58
+ }
59
+ function detectKeyKind(key) {
60
+ if (key.startsWith("ow_w_")) return "write";
61
+ if (key.startsWith("ow_r_")) return "read";
62
+ if (key.startsWith("ow_a_")) return "account";
63
+ if (LEGACY_RE.test(key)) return "legacy";
64
+ return "unknown";
65
+ }
66
+ function kindCanWrite(kind) {
67
+ return kind === "write" || kind === "legacy";
68
+ }
69
+ function pinExpectation(kind) {
70
+ switch (kind) {
71
+ case "read":
72
+ return "never";
73
+ case "account":
74
+ return "never";
75
+ case "write":
76
+ return "optional";
77
+ case "legacy":
78
+ return "required-for-private-reads";
79
+ default:
80
+ return "optional";
81
+ }
82
+ }
55
83
 
56
- // src/types.ts
57
- var ElementType = /* @__PURE__ */ ((ElementType2) => {
58
- ElementType2["Ability"] = "ability";
59
- ElementType2["Character"] = "character";
60
- ElementType2["Collective"] = "collective";
61
- ElementType2["Construct"] = "construct";
62
- ElementType2["Creature"] = "creature";
63
- ElementType2["Event"] = "event";
64
- ElementType2["Family"] = "family";
65
- ElementType2["Institution"] = "institution";
66
- ElementType2["Language"] = "language";
67
- ElementType2["Law"] = "law";
68
- ElementType2["Location"] = "location";
69
- ElementType2["Map"] = "map";
70
- ElementType2["Marker"] = "marker";
71
- ElementType2["Narrative"] = "narrative";
72
- ElementType2["Object"] = "object";
73
- ElementType2["Phenomenon"] = "phenomenon";
74
- ElementType2["Pin"] = "pin";
75
- ElementType2["Relation"] = "relation";
76
- ElementType2["Species"] = "species";
77
- ElementType2["Title"] = "title";
78
- ElementType2["Trait"] = "trait";
79
- ElementType2["Zone"] = "zone";
80
- return ElementType2;
81
- })(ElementType || {});
82
- var ELEMENT_LABELS = {
83
- ["ability" /* Ability */]: "Abilities",
84
- ["character" /* Character */]: "Characters",
85
- ["collective" /* Collective */]: "Collectives",
86
- ["construct" /* Construct */]: "Constructs",
87
- ["creature" /* Creature */]: "Creatures",
88
- ["event" /* Event */]: "Events",
89
- ["family" /* Family */]: "Families",
90
- ["institution" /* Institution */]: "Institutions",
91
- ["language" /* Language */]: "Languages",
92
- ["law" /* Law */]: "Laws",
93
- ["location" /* Location */]: "Locations",
94
- ["map" /* Map */]: "Maps",
95
- ["marker" /* Marker */]: "Markers",
96
- ["narrative" /* Narrative */]: "Narratives",
97
- ["object" /* Object */]: "Objects",
98
- ["phenomenon" /* Phenomenon */]: "Phenomena",
99
- ["pin" /* Pin */]: "Pins",
100
- ["relation" /* Relation */]: "Relations",
101
- ["species" /* Species */]: "Species",
102
- ["title" /* Title */]: "Titles",
103
- ["trait" /* Trait */]: "Traits",
104
- ["zone" /* Zone */]: "Zones"
84
+ // src/v2/client.ts
85
+ var DEFAULT_BASE_URL = "https://www.onlyworlds.com/api/v2";
86
+ var DEFAULT_PAGE_SIZE = 100;
87
+ var DEFAULT_CHANGES_PAGE_SIZE = 100;
88
+ var OwV2Client = class {
89
+ constructor(config) {
90
+ if (!config.apiKey) throw new Error("OwV2Client: apiKey is required");
91
+ this.apiKey = config.apiKey;
92
+ this.apiPin = config.apiPin || void 0;
93
+ this.keyKind = detectKeyKind(config.apiKey);
94
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
95
+ this.pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
96
+ this.changesPageSize = config.changesPageSize ?? DEFAULT_CHANGES_PAGE_SIZE;
97
+ this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
98
+ if (typeof this.fetchImpl !== "function") {
99
+ throw new Error("OwV2Client: no fetch available -- supply config.fetch");
100
+ }
101
+ }
102
+ // -- Health & world meta -------------------------------------------------
103
+ /** GET /health -- unauthenticated liveness pulse. */
104
+ async health() {
105
+ return this.request("GET", "/health/", { auth: false });
106
+ }
107
+ /**
108
+ * GET /world -- world meta (name, calendar/time fields, public_read).
109
+ * GOTCHA (by server design): world-meta edits do NOT appear in /changes and
110
+ * do not bump change_seq. Poll getWorld().updated_at for meta freshness.
111
+ */
112
+ async getWorld() {
113
+ return this.request("GET", "/world/");
114
+ }
115
+ /** PATCH /world -- partial world-meta update. */
116
+ async patchWorld(partial) {
117
+ return this.request("PATCH", "/world/", { body: sanitizePayload(partial) });
118
+ }
119
+ // -- Element CRUD --------------------------------------------------------
120
+ /** GET /{type}/ -- one cursor page. */
121
+ async list(type, params = {}) {
122
+ const query = buildQuery({
123
+ limit: params.limit ?? this.pageSize,
124
+ cursor: params.cursor,
125
+ expand: params.expand?.join(","),
126
+ fields: params.fields?.join(","),
127
+ ...params.filter
128
+ });
129
+ return this.request("GET", `/${type}/`, { query });
130
+ }
131
+ /** Cursor-walk every page of a type. Politeness: uses config pageSize. */
132
+ async *listAll(type, params = {}) {
133
+ let cursor;
134
+ do {
135
+ const page = await this.list(type, { ...params, cursor });
136
+ for (const el of page.data) yield el;
137
+ cursor = page.has_more && page.next_cursor ? page.next_cursor : void 0;
138
+ } while (cursor);
139
+ }
140
+ /** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
141
+ async get(type, id, opts = {}) {
142
+ const query = buildQuery({ expand: opts.expand?.join(","), fields: opts.fields?.join(",") });
143
+ return this.request("GET", `/${type}/${id}/`, { query });
144
+ }
145
+ /**
146
+ * POST /{type}/ -- create. Mints an RFC-4122 UUID for element.id when the
147
+ * caller omits one (design ruling D29d) so a retry carrying the same
148
+ * Idempotency-Key is structurally safe. Callers MAY still supply their own id.
149
+ */
150
+ async create(type, element, opts = {}) {
151
+ const body = sanitizePayload(element);
152
+ if (body.id === void 0 || body.id === null || body.id === "") {
153
+ body.id = mintUuid();
154
+ }
155
+ return this.request("POST", `/${type}/`, {
156
+ body,
157
+ idempotencyKey: opts.idempotencyKey
158
+ });
159
+ }
160
+ /** PUT /{type}/{id}/ -- upsert-by-client-id. The local-first write primitive. */
161
+ async upsert(type, id, element) {
162
+ return this.request("PUT", `/${type}/${id}/`, { body: sanitizePayload(element) });
163
+ }
164
+ /**
165
+ * PATCH /{type}/{id}/ -- partial update. DESTRUCTIVE on sent fields: arrays
166
+ * replace wholesale, omitted fields stay untouched. For link arrays prefer
167
+ * editLinks() -- atomic server-side merge, no read-before-write.
168
+ */
169
+ async patch(type, id, partial) {
170
+ return this.request("PATCH", `/${type}/${id}/`, { body: sanitizePayload(partial) });
171
+ }
172
+ /**
173
+ * DELETE /{type}/{id}/ -- idempotent (204 on absent). Server writes a
174
+ * tombstone AND scrubs the id from every other element's links in the same
175
+ * transaction -- no client-side unlink pass needed, ever.
176
+ */
177
+ async delete(type, id) {
178
+ await this.request("DELETE", `/${type}/${id}/`, { allowEmpty: true });
179
+ }
180
+ /**
181
+ * POST /{type}/{id}/links/{field} with {add, remove} -- atomic link merge.
182
+ * Dedupes, tolerates already-present/already-absent ids. Returns the FULL
183
+ * updated element (fixture P5). Use this for all relationship editing; it
184
+ * retires the read-merge-PATCH dance.
185
+ */
186
+ async editLinks(type, id, field, edit) {
187
+ return this.request("POST", `/${type}/${id}/links/${field}`, { body: edit });
188
+ }
189
+ // -- Bulk ----------------------------------------------------------------
190
+ /**
191
+ * POST /bulk -- up to ~1000 items. Partial success by default (HTTP 200
192
+ * always; inspect per-slot numeric `status` + top-level `errors` flag);
193
+ * atomic:true for all-or-nothing. Link validation runs against batch U
194
+ * database -- send in any order, cycles included; no client topo-sort.
195
+ * Success slots echo server-authoritative timestamps: set your sync baseline
196
+ * from this response alone. When an idempotencyKey is replayed, the returned
197
+ * response carries wasReplay:true (read from the Idempotent-Replay header).
198
+ */
199
+ async bulk(items, opts = {}) {
200
+ const body = {
201
+ items: items.map((it) => ({ type: it.type, element: sanitizePayload(it.element) })),
202
+ atomic: opts.atomic ?? false
203
+ };
204
+ return this.request("POST", "/bulk/", {
205
+ body,
206
+ idempotencyKey: opts.idempotencyKey,
207
+ replayAware: true
208
+ });
209
+ }
210
+ // -- Changes feed --------------------------------------------------------
211
+ /**
212
+ * GET /changes -- one page of the world's ordered change feed.
213
+ * Cursor is OPAQUE and never expires: persist verbatim, never parse.
214
+ * Zero/absent cursor = full export (byte-aligned with the Folder Format).
215
+ * Rewind rule: if your persisted position is ahead of page.head, the server
216
+ * was restored -- re-baseline from cursor zero; do not assume caught-up.
217
+ * Citizenship: heaviest route on the platform; default page size is polite.
218
+ */
219
+ async changes(opts = {}) {
220
+ const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize });
221
+ return this.request("GET", "/changes/", { query });
222
+ }
223
+ /**
224
+ * Walk the feed from `since` (or from zero = full export) to the current
225
+ * tail, yielding ops in order. Returns the final cursor via the generator's
226
+ * return value; persist it for the next incremental pull.
227
+ */
228
+ async *changesAll(since) {
229
+ let cursor = since;
230
+ let page;
231
+ do {
232
+ page = await this.changes({ since: cursor });
233
+ for (const op of page.changes) yield op;
234
+ cursor = page.cursor;
235
+ } while (page.has_more);
236
+ return { cursor: page.cursor, head: page.head };
237
+ }
238
+ // -- Core request machinery ----------------------------------------------
239
+ /**
240
+ * Raw authenticated request against this client's baseUrl. Public since 4.0
241
+ * so auxiliary resources can ride the same transport — it structurally
242
+ * satisfies `TokenTransport` (`new TokenResource(client)`). Prefer the typed
243
+ * methods for element CRUD; this is the escape hatch, and it does NOT apply
244
+ * sanitizePayload — callers own their body shape.
245
+ */
246
+ async request(method, path, opts = {}) {
247
+ const url = `${this.baseUrl}${path}${opts.query ? `?${opts.query}` : ""}`;
248
+ const headers = {};
249
+ if (opts.auth !== false) {
250
+ if (this.keyKind === "account") {
251
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
252
+ } else {
253
+ headers["API-Key"] = this.apiKey;
254
+ if (this.apiPin) headers["API-Pin"] = this.apiPin;
255
+ }
256
+ }
257
+ if (opts.body !== void 0) headers["Content-Type"] = "application/json";
258
+ if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
259
+ let res;
260
+ try {
261
+ res = await this.fetchImpl(url, {
262
+ method,
263
+ headers,
264
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
265
+ });
266
+ } catch (cause) {
267
+ throw new OwNetworkError(`OnlyWorlds request failed: ${method} ${path}`, cause);
268
+ }
269
+ if (!res.ok) throw await errorFromResponse(res);
270
+ if (res.status === 204 || opts.allowEmpty) {
271
+ const text = await res.text();
272
+ return text ? JSON.parse(text) : null;
273
+ }
274
+ const parsed = await res.json();
275
+ if (opts.replayAware && parsed && typeof parsed === "object") {
276
+ parsed.wasReplay = readReplayHeader(res.headers);
277
+ }
278
+ return parsed;
279
+ }
105
280
  };
106
- function getElementLabel(elementType) {
107
- return ELEMENT_LABELS[elementType];
281
+ var READ_ONLY_FIELDS = ["world", "type", "created_at", "updated_at", "change_seq"];
282
+ function sanitizePayload(payload) {
283
+ if (!payload || typeof payload !== "object") return payload;
284
+ const rest = { ...payload };
285
+ for (const field of READ_ONLY_FIELDS) delete rest[field];
286
+ return rest;
108
287
  }
288
+ function readReplayHeader(headers) {
289
+ const v = headers.get("Idempotent-Replay");
290
+ return v != null && v.toLowerCase() === "true";
291
+ }
292
+ function mintUuid() {
293
+ const c = globalThis.crypto;
294
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
295
+ const bytes = new Uint8Array(16);
296
+ if (c && typeof c.getRandomValues === "function") {
297
+ c.getRandomValues(bytes);
298
+ } else {
299
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
300
+ }
301
+ bytes[6] = bytes[6] & 15 | 64;
302
+ bytes[8] = bytes[8] & 63 | 128;
303
+ const hex = [];
304
+ for (let i = 0; i < 256; i++) hex.push((i + 256).toString(16).slice(1));
305
+ return hex[bytes[0]] + hex[bytes[1]] + hex[bytes[2]] + hex[bytes[3]] + "-" + hex[bytes[4]] + hex[bytes[5]] + "-" + hex[bytes[6]] + hex[bytes[7]] + "-" + hex[bytes[8]] + hex[bytes[9]] + "-" + hex[bytes[10]] + hex[bytes[11]] + hex[bytes[12]] + hex[bytes[13]] + hex[bytes[14]] + hex[bytes[15]];
306
+ }
307
+ function buildQuery(params) {
308
+ const q = new URLSearchParams();
309
+ for (const [k, v] of Object.entries(params)) {
310
+ if (v !== void 0 && v !== null && v !== "") q.set(k, String(v));
311
+ }
312
+ return q.toString();
313
+ }
314
+
315
+ // src/v2/types.generated.ts
316
+ var ELEMENT_TYPES = ["ability", "character", "collective", "construct", "creature", "event", "family", "institution", "language", "law", "location", "map", "marker", "narrative", "object", "phenomenon", "pin", "relation", "species", "title", "trait", "zone"];
317
+ var ONLYWORLDS_VERSION = "00.30.00";
318
+ var ELEMENT_FAMILIES = {
319
+ ability: "abstract",
320
+ character: "agents",
321
+ collective: "agents",
322
+ construct: "world",
323
+ creature: "agents",
324
+ event: "temporal",
325
+ family: "agents",
326
+ institution: "agents",
327
+ language: "abstract",
328
+ law: "abstract",
329
+ location: "world",
330
+ map: "world",
331
+ marker: "world",
332
+ narrative: "temporal",
333
+ object: "world",
334
+ phenomenon: "temporal",
335
+ pin: "world",
336
+ relation: "temporal",
337
+ species: "agents",
338
+ title: "abstract",
339
+ trait: "abstract",
340
+ zone: "world"
341
+ };
342
+ var ELEMENT_ICONS = {
343
+ ability: "auto_fix_normal",
344
+ character: "person",
345
+ collective: "groups_3",
346
+ construct: "api",
347
+ creature: "bug_report",
348
+ event: "saved_search",
349
+ family: "supervisor_account",
350
+ institution: "business",
351
+ language: "edit_road",
352
+ law: "gpp_bad",
353
+ location: "castle",
354
+ map: "map",
355
+ marker: "place",
356
+ narrative: "menu_book",
357
+ object: "webhook",
358
+ phenomenon: "thunderstorm",
359
+ pin: "push_pin",
360
+ relation: "link",
361
+ species: "crib",
362
+ title: "military_tech",
363
+ trait: "flaky",
364
+ zone: "architecture"
365
+ };
109
366
  var ELEMENT_SECTIONS = {
110
- ["ability" /* Ability */]: [
367
+ ability: [
111
368
  { name: "Mechanics", order: 1, fields: ["activation", "duration", "potency", "range", "effects", "challenges", "talents", "requisites"] },
112
369
  { name: "World", order: 2, fields: ["prevalence", "tradition", "source", "locus", "instruments", "systems"] }
113
370
  ],
114
- ["character" /* Character */]: [
371
+ character: [
115
372
  { name: "Constitution", order: 1, fields: ["physicality", "mentality", "height", "weight", "species", "traits", "abilities"] },
116
373
  { name: "Origins", order: 2, fields: ["background", "motivations", "birth_date", "birthplace", "languages"] },
117
374
  { name: "World", order: 3, fields: ["reputation", "location", "objects", "institutions"] },
@@ -119,43 +376,43 @@ var ELEMENT_SECTIONS = {
119
376
  { name: "Social", order: 5, fields: ["family", "friends", "rivals"] },
120
377
  { name: "TTRPG", order: 6, fields: ["level", "hit_points", "STR", "DEX", "CON", "INT", "WIS", "CHA"] }
121
378
  ],
122
- ["collective" /* Collective */]: [
379
+ collective: [
123
380
  { name: "Formation", order: 1, fields: ["composition", "count", "formation_date", "operator", "equipment"] },
124
381
  { name: "Dynamics", order: 2, fields: ["activity", "disposition", "state", "abilities", "symbolism"] },
125
382
  { name: "World", order: 3, fields: ["species", "characters", "creatures", "phenomena"] }
126
383
  ],
127
- ["construct" /* Construct */]: [
384
+ construct: [
128
385
  { name: "Nature", order: 1, fields: ["rationale", "history", "status", "reach", "start_date", "end_date", "founder", "custodian"] },
129
386
  { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs", "events", "narratives"] }
130
387
  ],
131
- ["creature" /* Creature */]: [
388
+ creature: [
132
389
  { name: "Biology", order: 1, fields: ["appearance", "weight", "height", "species"] },
133
- { name: "Behaviour", order: 2, fields: ["habits", "demeanor", "traits", "abilities", "languages"] },
390
+ { name: "Behavior", order: 2, fields: ["habits", "demeanor", "traits", "abilities", "languages"] },
134
391
  { name: "World", order: 3, fields: ["status", "birth_date", "location", "zone"] },
135
392
  { name: "TTRPG", order: 4, fields: ["challenge_rating", "hit_points", "armor_class", "speed", "actions"] }
136
393
  ],
137
- ["event" /* Event */]: [
394
+ event: [
138
395
  { name: "Nature", order: 1, fields: ["history", "challenges", "consequences", "start_date", "end_date", "triggers"] },
139
396
  { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs"] }
140
397
  ],
141
- ["family" /* Family */]: [
398
+ family: [
142
399
  { name: "Identity", order: 1, fields: ["spirit", "history", "traditions", "traits", "abilities", "languages", "ancestors"] },
143
400
  { name: "World", order: 2, fields: ["reputation", "estates", "governs", "heirlooms", "creatures"] }
144
401
  ],
145
- ["institution" /* Institution */]: [
402
+ institution: [
146
403
  { name: "Foundation", order: 1, fields: ["doctrine", "founding_date", "parent_institution"] },
147
404
  { name: "Claims", order: 2, fields: ["zones", "objects", "creatures"] },
148
405
  { name: "World", order: 3, fields: ["status", "allies", "adversaries", "constructs"] }
149
406
  ],
150
- ["language" /* Language */]: [
407
+ language: [
151
408
  { name: "Structure", order: 1, fields: ["phonology", "grammar", "lexicon", "writing", "classification"] },
152
409
  { name: "World", order: 2, fields: ["status", "spread", "dialects"] }
153
410
  ],
154
- ["law" /* Law */]: [
411
+ law: [
155
412
  { name: "Code", order: 1, fields: ["declaration", "purpose", "date", "parent_law", "penalties"] },
156
413
  { name: "World", order: 2, fields: ["author", "locations", "zones", "prohibitions", "adjudicators", "enforcers"] }
157
414
  ],
158
- ["location" /* Location */]: [
415
+ location: [
159
416
  { name: "Setting", order: 1, fields: ["form", "function", "founding_date", "parent_location", "populations"] },
160
417
  { name: "Politics", order: 2, fields: ["political_climate", "primary_power", "governing_title", "secondary_powers", "zone", "rival", "partner"] },
161
418
  { name: "World", order: 3, fields: ["customs", "founders", "cults", "delicacies"] },
@@ -164,78 +421,94 @@ var ELEMENT_SECTIONS = {
164
421
  { name: "Construction", order: 6, fields: ["architecture", "buildings", "building_methods"] },
165
422
  { name: "Defense", order: 7, fields: ["defensibility", "elevation", "fighters", "defensive_objects"] }
166
423
  ],
167
- ["map" /* Map */]: [
424
+ map: [
168
425
  { name: "Details", order: 1, fields: ["background_color", "hierarchy", "width", "height", "depth", "parent_map", "location"] }
169
426
  ],
170
- ["marker" /* Marker */]: [
427
+ marker: [
171
428
  { name: "Details", order: 1, fields: ["map", "zone", "x", "y", "z", "order"] }
172
429
  ],
173
- ["narrative" /* Narrative */]: [
430
+ narrative: [
174
431
  { name: "Context", order: 1, fields: ["story", "consequences", "start_date", "end_date", "order", "parent_narrative", "protagonist", "antagonist", "narrator", "conservator"] },
175
432
  { name: "Involves", order: 2, fields: ["events", "characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs", "laws"] }
176
433
  ],
177
- ["object" /* Object */]: [
434
+ object: [
178
435
  { name: "Form", order: 1, fields: ["aesthetics", "weight", "amount", "parent_object", "materials", "technology"] },
179
436
  { name: "Function", order: 2, fields: ["utility", "effects", "abilities", "consumes"] },
180
437
  { name: "World", order: 3, fields: ["origins", "location", "language", "affinities"] }
181
438
  ],
182
- ["phenomenon" /* Phenomenon */]: [
439
+ phenomenon: [
183
440
  { name: "Mechanics", order: 1, fields: ["expression", "effects", "duration", "catalysts", "empowerments"] },
184
441
  { name: "World", order: 2, fields: ["mythology", "system", "triggers", "wielders", "environments"] }
185
442
  ],
186
- ["pin" /* Pin */]: [
187
- { name: "Details", order: 1, fields: ["map", "element_type", "element_id", "element", "x", "y", "z"] }
443
+ pin: [
444
+ { name: "Details", order: 1, fields: ["map", "element", "x", "y", "z"] }
188
445
  ],
189
- ["relation" /* Relation */]: [
446
+ relation: [
190
447
  { name: "Nature", order: 1, fields: ["background", "start_date", "end_date", "intensity", "actor", "events"] },
191
- { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs", "events", "narratives"] }
448
+ { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "titles", "constructs", "events", "narratives"] }
192
449
  ],
193
- ["species" /* Species */]: [
450
+ species: [
194
451
  { name: "Biology", order: 1, fields: ["appearance", "life_span", "weight", "nourishment", "reproduction", "adaptations"] },
195
452
  { name: "Psychology", order: 2, fields: ["instincts", "sociality", "temperament", "communication", "aggression", "traits"] },
196
453
  { name: "World", order: 3, fields: ["role", "parent_species", "locations", "zones", "affinities"] }
197
454
  ],
198
- ["title" /* Title */]: [
455
+ title: [
199
456
  { name: "Mandate", order: 1, fields: ["authority", "eligibility", "grant_date", "revoke_date", "issuer", "body", "superior_title", "holders", "symbols"] },
200
457
  { name: "World", order: 2, fields: ["status", "history", "characters", "institutions", "families", "zones", "locations", "objects", "constructs", "laws", "collectives", "creatures", "phenomena", "species", "languages"] }
201
458
  ],
202
- ["trait" /* Trait */]: [
459
+ trait: [
203
460
  { name: "Qualitative", order: 1, fields: ["social_effects", "physical_effects", "functional_effects", "personality_effects", "behaviour_effects"] },
204
461
  { name: "Quantitative", order: 2, fields: ["charisma", "coercion", "competence", "compassion", "creativity", "courage"] },
205
462
  { name: "World", order: 3, fields: ["significance", "anti_trait", "empowered_abilities"] }
206
463
  ],
207
- ["zone" /* Zone */]: [
464
+ zone: [
208
465
  { name: "Scope", order: 1, fields: ["role", "start_date", "end_date", "phenomena", "linked_zones"] },
209
466
  { name: "World", order: 2, fields: ["context", "populations", "titles", "principles"] }
210
467
  ]
211
468
  };
212
- function getElementSections(elementType) {
213
- return ELEMENT_SECTIONS[elementType] || [];
469
+
470
+ // src/v2/types.ts
471
+ var SPATIAL_TYPES = ["map", "pin", "marker", "zone"];
472
+
473
+ // src/v2/palette.ts
474
+ var FAMILY_COLORS = {
475
+ agents: { light: "#2a78d6", dark: "#3987e5" },
476
+ world: { light: "#008300", dark: "#008300" },
477
+ abstract: { light: "#e87ba4", dark: "#d55181" },
478
+ temporal: { light: "#eda100", dark: "#c98500" }
479
+ };
480
+ function familyOf(type) {
481
+ return ELEMENT_FAMILIES[type];
214
482
  }
215
- var ONLYWORLDS_VERSION = "00.30.00";
216
- var ELEMENT_ICONS = {
217
- ["ability" /* Ability */]: "auto_fix_normal",
218
- ["character" /* Character */]: "person",
219
- ["collective" /* Collective */]: "groups_3",
220
- ["construct" /* Construct */]: "api",
221
- ["creature" /* Creature */]: "bug_report",
222
- ["event" /* Event */]: "saved_search",
223
- ["family" /* Family */]: "supervisor_account",
224
- ["institution" /* Institution */]: "business",
225
- ["language" /* Language */]: "edit_road",
226
- ["law" /* Law */]: "gpp_bad",
227
- ["location" /* Location */]: "castle",
228
- ["map" /* Map */]: "map",
229
- ["marker" /* Marker */]: "place",
230
- ["narrative" /* Narrative */]: "menu_book",
231
- ["object" /* Object */]: "webhook",
232
- ["phenomenon" /* Phenomenon */]: "thunderstorm",
233
- ["pin" /* Pin */]: "push_pin",
234
- ["relation" /* Relation */]: "link",
235
- ["species" /* Species */]: "crib",
236
- ["title" /* Title */]: "military_tech",
237
- ["trait" /* Trait */]: "flaky",
238
- ["zone" /* Zone */]: "architecture"
483
+ function elementColor(type, mode = "dark") {
484
+ return FAMILY_COLORS[ELEMENT_FAMILIES[type]][mode];
485
+ }
486
+ var FAMILY_ORDER = ["agents", "world", "abstract", "temporal"];
487
+
488
+ // src/v2/constants.ts
489
+ var ELEMENT_LABELS = {
490
+ ability: "Abilities",
491
+ character: "Characters",
492
+ collective: "Collectives",
493
+ construct: "Constructs",
494
+ creature: "Creatures",
495
+ event: "Events",
496
+ family: "Families",
497
+ institution: "Institutions",
498
+ language: "Languages",
499
+ law: "Laws",
500
+ location: "Locations",
501
+ map: "Maps",
502
+ marker: "Markers",
503
+ narrative: "Narratives",
504
+ object: "Objects",
505
+ phenomenon: "Phenomena",
506
+ pin: "Pins",
507
+ relation: "Relations",
508
+ species: "Species",
509
+ title: "Titles",
510
+ trait: "Traits",
511
+ zone: "Zones"
239
512
  };
240
513
  var FIELD_SCHEMA = {
241
514
  ability: {
@@ -247,9 +520,9 @@ var FIELD_SCHEMA = {
247
520
  image_url: { type: "text", required: false },
248
521
  // Mechanics
249
522
  activation: { type: "text" },
250
- duration: { type: "number" },
251
- potency: { type: "number" },
252
- range: { type: "number" },
523
+ duration: { type: "integer" },
524
+ potency: { type: "integer" },
525
+ range: { type: "integer" },
253
526
  effects: { type: "multi_link", target: "phenomenon" },
254
527
  challenges: { type: "text" },
255
528
  talents: { type: "multi_link", target: "trait" },
@@ -272,15 +545,15 @@ var FIELD_SCHEMA = {
272
545
  // Constitution
273
546
  physicality: { type: "text" },
274
547
  mentality: { type: "text" },
275
- height: { type: "number" },
276
- weight: { type: "number" },
548
+ height: { type: "integer" },
549
+ weight: { type: "integer" },
277
550
  species: { type: "multi_link", target: "species" },
278
551
  traits: { type: "multi_link", target: "trait" },
279
552
  abilities: { type: "multi_link", target: "ability" },
280
553
  // Origins
281
554
  background: { type: "text" },
282
555
  motivations: { type: "text" },
283
- birth_date: { type: "number" },
556
+ birth_date: { type: "integer" },
284
557
  birthplace: { type: "single_link", target: "location" },
285
558
  languages: { type: "multi_link", target: "language" },
286
559
  // World
@@ -289,25 +562,25 @@ var FIELD_SCHEMA = {
289
562
  objects: { type: "multi_link", target: "object" },
290
563
  institutions: { type: "multi_link", target: "institution" },
291
564
  // Personality
292
- charisma: { type: "number" },
293
- coercion: { type: "number" },
294
- competence: { type: "number" },
295
- compassion: { type: "number" },
296
- creativity: { type: "number" },
297
- courage: { type: "number" },
565
+ charisma: { type: "integer" },
566
+ coercion: { type: "integer" },
567
+ competence: { type: "integer" },
568
+ compassion: { type: "integer" },
569
+ creativity: { type: "integer" },
570
+ courage: { type: "integer" },
298
571
  // Social
299
572
  family: { type: "multi_link", target: "family" },
300
573
  friends: { type: "multi_link", target: "character" },
301
574
  rivals: { type: "multi_link", target: "character" },
302
575
  // TTRPG
303
- level: { type: "number" },
304
- hit_points: { type: "number" },
305
- STR: { type: "number" },
306
- DEX: { type: "number" },
307
- CON: { type: "number" },
308
- INT: { type: "number" },
309
- WIS: { type: "number" },
310
- CHA: { type: "number" }
576
+ level: { type: "integer" },
577
+ hit_points: { type: "integer" },
578
+ STR: { type: "integer" },
579
+ DEX: { type: "integer" },
580
+ CON: { type: "integer" },
581
+ INT: { type: "integer" },
582
+ WIS: { type: "integer" },
583
+ CHA: { type: "integer" }
311
584
  },
312
585
  collective: {
313
586
  // Base fields (shared by all elements)
@@ -318,8 +591,8 @@ var FIELD_SCHEMA = {
318
591
  image_url: { type: "text", required: false },
319
592
  // Formation
320
593
  composition: { type: "text" },
321
- count: { type: "number" },
322
- formation_date: { type: "number" },
594
+ count: { type: "integer" },
595
+ formation_date: { type: "integer" },
323
596
  operator: { type: "single_link", target: "institution" },
324
597
  equipment: { type: "multi_link", target: "construct" },
325
598
  // Dynamics
@@ -346,8 +619,8 @@ var FIELD_SCHEMA = {
346
619
  history: { type: "text" },
347
620
  status: { type: "text" },
348
621
  reach: { type: "text" },
349
- start_date: { type: "number" },
350
- end_date: { type: "number" },
622
+ start_date: { type: "integer" },
623
+ end_date: { type: "integer" },
351
624
  founder: { type: "single_link", target: "character" },
352
625
  custodian: { type: "single_link", target: "institution" },
353
626
  // Involves
@@ -379,8 +652,8 @@ var FIELD_SCHEMA = {
379
652
  image_url: { type: "text", required: false },
380
653
  // Biology
381
654
  appearance: { type: "text" },
382
- weight: { type: "number" },
383
- height: { type: "number" },
655
+ weight: { type: "integer" },
656
+ height: { type: "integer" },
384
657
  species: { type: "multi_link", target: "species" },
385
658
  // Behaviour
386
659
  habits: { type: "text" },
@@ -390,14 +663,14 @@ var FIELD_SCHEMA = {
390
663
  languages: { type: "multi_link", target: "language" },
391
664
  // World
392
665
  status: { type: "text" },
393
- birth_date: { type: "number" },
666
+ birth_date: { type: "integer" },
394
667
  location: { type: "single_link", target: "location" },
395
668
  zone: { type: "single_link", target: "zone" },
396
669
  // TTRPG
397
- challenge_rating: { type: "number" },
398
- hit_points: { type: "number" },
399
- armor_class: { type: "number" },
400
- speed: { type: "number" },
670
+ challenge_rating: { type: "integer" },
671
+ hit_points: { type: "integer" },
672
+ armor_class: { type: "integer" },
673
+ speed: { type: "integer" },
401
674
  actions: { type: "multi_link", target: "ability" }
402
675
  },
403
676
  event: {
@@ -411,8 +684,8 @@ var FIELD_SCHEMA = {
411
684
  history: { type: "text" },
412
685
  challenges: { type: "text" },
413
686
  consequences: { type: "text" },
414
- start_date: { type: "number" },
415
- end_date: { type: "number" },
687
+ start_date: { type: "integer" },
688
+ end_date: { type: "integer" },
416
689
  triggers: { type: "multi_link", target: "event" },
417
690
  // Involves
418
691
  characters: { type: "multi_link", target: "character" },
@@ -463,7 +736,7 @@ var FIELD_SCHEMA = {
463
736
  image_url: { type: "text", required: false },
464
737
  // Foundation
465
738
  doctrine: { type: "text" },
466
- founding_date: { type: "number" },
739
+ founding_date: { type: "integer" },
467
740
  parent_institution: { type: "single_link", target: "institution" },
468
741
  // Claims
469
742
  zones: { type: "multi_link", target: "zone" },
@@ -503,7 +776,7 @@ var FIELD_SCHEMA = {
503
776
  // Code
504
777
  declaration: { type: "text" },
505
778
  purpose: { type: "text" },
506
- date: { type: "number" },
779
+ date: { type: "integer" },
507
780
  parent_law: { type: "single_link", target: "law" },
508
781
  penalties: { type: "multi_link", target: "construct" },
509
782
  // World
@@ -524,7 +797,7 @@ var FIELD_SCHEMA = {
524
797
  // Setting
525
798
  form: { type: "text" },
526
799
  function: { type: "text" },
527
- founding_date: { type: "number" },
800
+ founding_date: { type: "integer" },
528
801
  parent_location: { type: "single_link", target: "location" },
529
802
  populations: { type: "multi_link", target: "collective" },
530
803
  // Politics
@@ -556,7 +829,7 @@ var FIELD_SCHEMA = {
556
829
  building_methods: { type: "multi_link", target: "construct" },
557
830
  // Defense
558
831
  defensibility: { type: "text" },
559
- elevation: { type: "number" },
832
+ elevation: { type: "integer" },
560
833
  fighters: { type: "multi_link", target: "construct" },
561
834
  defensive_objects: { type: "multi_link", target: "object" }
562
835
  },
@@ -569,10 +842,10 @@ var FIELD_SCHEMA = {
569
842
  image_url: { type: "text", required: false },
570
843
  // Details
571
844
  background_color: { type: "text" },
572
- hierarchy: { type: "number" },
573
- width: { type: "number" },
574
- height: { type: "number" },
575
- depth: { type: "number" },
845
+ hierarchy: { type: "integer" },
846
+ width: { type: "integer" },
847
+ height: { type: "integer" },
848
+ depth: { type: "integer" },
576
849
  parent_map: { type: "single_link", target: "map" },
577
850
  location: { type: "single_link", target: "location" }
578
851
  },
@@ -586,10 +859,10 @@ var FIELD_SCHEMA = {
586
859
  // Details
587
860
  map: { type: "single_link", target: "map", required: true },
588
861
  zone: { type: "single_link", target: "zone", required: true },
589
- x: { type: "number", required: true },
590
- y: { type: "number", required: true },
591
- z: { type: "number" },
592
- order: { type: "number", required: true }
862
+ x: { type: "integer", required: true },
863
+ y: { type: "integer", required: true },
864
+ z: { type: "integer" },
865
+ order: { type: "integer", required: true }
593
866
  },
594
867
  narrative: {
595
868
  // Base fields (shared by all elements)
@@ -601,9 +874,9 @@ var FIELD_SCHEMA = {
601
874
  // Context
602
875
  story: { type: "text" },
603
876
  consequences: { type: "text" },
604
- start_date: { type: "number" },
605
- end_date: { type: "number" },
606
- order: { type: "number" },
877
+ start_date: { type: "integer" },
878
+ end_date: { type: "integer" },
879
+ order: { type: "integer" },
607
880
  parent_narrative: { type: "single_link", target: "narrative" },
608
881
  protagonist: { type: "single_link", target: "character" },
609
882
  antagonist: { type: "single_link", target: "character" },
@@ -638,8 +911,8 @@ var FIELD_SCHEMA = {
638
911
  image_url: { type: "text", required: false },
639
912
  // Form
640
913
  aesthetics: { type: "text" },
641
- weight: { type: "number" },
642
- amount: { type: "number" },
914
+ weight: { type: "integer" },
915
+ amount: { type: "integer" },
643
916
  parent_object: { type: "single_link", target: "object" },
644
917
  materials: { type: "multi_link", target: "construct" },
645
918
  technology: { type: "multi_link", target: "construct" },
@@ -664,7 +937,7 @@ var FIELD_SCHEMA = {
664
937
  // Mechanics
665
938
  expression: { type: "text" },
666
939
  effects: { type: "text" },
667
- duration: { type: "number" },
940
+ duration: { type: "integer" },
668
941
  catalysts: { type: "multi_link", target: "object" },
669
942
  empowerments: { type: "multi_link", target: "ability" },
670
943
  // World
@@ -687,9 +960,9 @@ var FIELD_SCHEMA = {
687
960
  // ElementType enum value; YAML 'element' generic-link is split into _type + _id
688
961
  element_id: { type: "single_link", target: "any", required: true },
689
962
  // Can reference any element
690
- x: { type: "number", required: true },
691
- y: { type: "number", required: true },
692
- z: { type: "number" }
963
+ x: { type: "integer", required: true },
964
+ y: { type: "integer", required: true },
965
+ z: { type: "integer" }
693
966
  },
694
967
  relation: {
695
968
  // Base fields (shared by all elements)
@@ -700,9 +973,9 @@ var FIELD_SCHEMA = {
700
973
  image_url: { type: "text", required: false },
701
974
  // Nature
702
975
  background: { type: "text" },
703
- start_date: { type: "number" },
704
- end_date: { type: "number" },
705
- intensity: { type: "number" },
976
+ start_date: { type: "integer" },
977
+ end_date: { type: "integer" },
978
+ intensity: { type: "integer" },
706
979
  actor: { type: "single_link", target: "character" },
707
980
  events: { type: "multi_link", target: "event" },
708
981
  // Involves
@@ -733,8 +1006,8 @@ var FIELD_SCHEMA = {
733
1006
  image_url: { type: "text", required: false },
734
1007
  // Biology
735
1008
  appearance: { type: "text" },
736
- life_span: { type: "number" },
737
- weight: { type: "number" },
1009
+ life_span: { type: "integer" },
1010
+ weight: { type: "integer" },
738
1011
  nourishment: { type: "multi_link", target: "species" },
739
1012
  reproduction: { type: "multi_link", target: "construct" },
740
1013
  adaptations: { type: "multi_link", target: "ability" },
@@ -743,7 +1016,7 @@ var FIELD_SCHEMA = {
743
1016
  sociality: { type: "text" },
744
1017
  temperament: { type: "text" },
745
1018
  communication: { type: "text" },
746
- aggression: { type: "number" },
1019
+ aggression: { type: "integer" },
747
1020
  traits: { type: "multi_link", target: "trait" },
748
1021
  // World
749
1022
  role: { type: "text" },
@@ -762,8 +1035,8 @@ var FIELD_SCHEMA = {
762
1035
  // Mandate
763
1036
  authority: { type: "text" },
764
1037
  eligibility: { type: "text" },
765
- grant_date: { type: "number" },
766
- revoke_date: { type: "number" },
1038
+ grant_date: { type: "integer" },
1039
+ revoke_date: { type: "integer" },
767
1040
  issuer: { type: "single_link", target: "institution" },
768
1041
  body: { type: "single_link", target: "institution" },
769
1042
  superior_title: { type: "single_link", target: "title" },
@@ -777,856 +1050,295 @@ var FIELD_SCHEMA = {
777
1050
  families: { type: "multi_link", target: "family" },
778
1051
  zones: { type: "multi_link", target: "zone" },
779
1052
  locations: { type: "multi_link", target: "location" },
780
- objects: { type: "multi_link", target: "object" },
781
- constructs: { type: "multi_link", target: "construct" },
782
- laws: { type: "multi_link", target: "law" },
783
- collectives: { type: "multi_link", target: "collective" },
784
- creatures: { type: "multi_link", target: "creature" },
785
- phenomena: { type: "multi_link", target: "phenomenon" },
786
- species: { type: "multi_link", target: "species" },
787
- languages: { type: "multi_link", target: "language" }
788
- },
789
- trait: {
790
- // Base fields (shared by all elements)
791
- name: { type: "text", required: true },
792
- description: { type: "text", required: false },
793
- supertype: { type: "text", required: false },
794
- subtype: { type: "text", required: false },
795
- image_url: { type: "text", required: false },
796
- // Qualitative
797
- social_effects: { type: "text" },
798
- physical_effects: { type: "text" },
799
- functional_effects: { type: "text" },
800
- personality_effects: { type: "text" },
801
- behaviour_effects: { type: "text" },
802
- // Quantitative
803
- charisma: { type: "number" },
804
- coercion: { type: "number" },
805
- competence: { type: "number" },
806
- compassion: { type: "number" },
807
- creativity: { type: "number" },
808
- courage: { type: "number" },
809
- // World
810
- significance: { type: "text" },
811
- anti_trait: { type: "single_link", target: "trait" },
812
- empowered_abilities: { type: "multi_link", target: "ability" }
813
- },
814
- zone: {
815
- // Base fields (shared by all elements)
816
- name: { type: "text", required: true },
817
- description: { type: "text", required: false },
818
- supertype: { type: "text", required: false },
819
- subtype: { type: "text", required: false },
820
- image_url: { type: "text", required: false },
821
- // Scope
822
- role: { type: "text" },
823
- start_date: { type: "number" },
824
- end_date: { type: "number" },
825
- phenomena: { type: "multi_link", target: "phenomenon" },
826
- linked_zones: { type: "multi_link", target: "zone" },
827
- // World
828
- context: { type: "text" },
829
- populations: { type: "multi_link", target: "collective" },
830
- titles: { type: "multi_link", target: "title" },
831
- principles: { type: "multi_link", target: "construct" }
832
- }
833
- };
834
- function createElementId(id) {
835
- return id;
836
- }
837
- function createElementIds(ids) {
838
- return ids;
839
- }
840
- function createAnyElementId(id) {
841
- return id;
842
- }
843
-
844
- // src/token-resource.ts
845
- var TokenResource = class {
846
- constructor(client) {
847
- this.client = client;
848
- }
849
- /**
850
- * Get current token status for authenticated user
851
- *
852
- * Returns daily token allowance, usage, and availability.
853
- * Matches base-tool's checkStatus() pattern.
854
- *
855
- * @returns Current token status
856
- * @example
857
- * ```typescript
858
- * const status = await client.tokens.getStatus();
859
- * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
860
- * console.log(`Used today: ${status.tokens_used_today}`);
861
- * console.log(`Active sessions: ${status.sessions_active}`);
862
- * ```
863
- */
864
- async getStatus() {
865
- return this.client.request("GET", "/tokens/status/");
866
- }
867
- /**
868
- * Consume tokens for service usage
869
- *
870
- * Reports token consumption to track daily usage. Allows consumption even if
871
- * exceeds available tokens (tracks as debt), but warns via error field.
872
- * Matches base-tool's reportUsage() pattern.
873
- *
874
- * @param params - Token consumption parameters
875
- * @returns Consumption result with updated balance
876
- * @example
877
- * ```typescript
878
- * const result = await client.tokens.consume({
879
- * amount: 500,
880
- * service: 'worldbuilding_tool',
881
- * metadata: {
882
- * feature: 'character_generation',
883
- * model: 'gpt-4',
884
- * prompt_tokens: 300,
885
- * completion_tokens: 200
886
- * }
887
- * });
888
- *
889
- * if (result.error) {
890
- * console.warn('Token warning:', result.error);
891
- * }
892
- * console.log(`${result.tokens_remaining} tokens remaining`);
893
- * ```
894
- */
895
- async consume(params) {
896
- return this.client.request("POST", "/tokens/consume/", {
897
- body: {
898
- amount: params.amount,
899
- service: params.service || "sdk_client",
900
- session_id: params.sessionId ?? null,
901
- metadata: params.metadata ?? null
902
- }
903
- });
904
- }
905
- /**
906
- * Get encrypted OpenAI API key (advanced use case)
907
- *
908
- * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
909
- * Returns encrypted key that must be decrypted client-side using Fernet.
910
- *
911
- * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
912
- * including client-side decryption with the 'fernet' npm package.
913
- *
914
- * @returns Encrypted access key and session info
915
- * @throws Error if insufficient tokens (< 100)
916
- * @example
917
- * ```typescript
918
- * // Get encrypted key
919
- * const access = await client.tokens.getAccessKey();
920
- *
921
- * // Decrypt using fernet library (see base-tool for full example)
922
- * // 1. Derive key from world ID using SHA-256
923
- * // 2. Use 'fernet' npm package to decrypt
924
- * // 3. Use decrypted OpenAI key for direct API calls
925
- * // 4. Report usage with access.session_id
926
- *
927
- * console.log('Session:', access.session_id);
928
- * console.log('Expires:', access.expires_at);
929
- * ```
930
- */
931
- async getAccessKey() {
932
- return this.client.request("GET", "/tokens/access-key/");
933
- }
934
- /**
935
- * Revoke a specific token session
936
- *
937
- * Invalidates the session ID obtained from getAccessKey().
938
- * Use when cleaning up or on logout.
939
- *
940
- * @param sessionId - Session ID to revoke
941
- * @returns Revocation result
942
- * @example
943
- * ```typescript
944
- * await client.tokens.revokeSession('session-id-here');
945
- * ```
946
- */
947
- async revokeSession(sessionId) {
948
- return this.client.request(
949
- "POST",
950
- `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
951
- );
952
- }
953
- /**
954
- * Revoke all active sessions (emergency use)
955
- *
956
- * Invalidates all token sessions for the authenticated user.
957
- * Use for security cleanup or when sessions are stuck.
958
- *
959
- * @returns Revocation result with count of revoked sessions
960
- * @example
961
- * ```typescript
962
- * const result = await client.tokens.revokeAllSessions();
963
- * console.log(`Revoked ${result.sessions_revoked} sessions`);
964
- * ```
965
- */
966
- async revokeAllSessions() {
967
- return this.client.request("POST", "/tokens/revoke-all-sessions/");
968
- }
969
- /**
970
- * Get public encryption info (no auth required)
971
- *
972
- * Returns algorithm details and example code for client-side decryption.
973
- * Public endpoint - can be called without authentication.
974
- *
975
- * @returns Encryption algorithm and implementation details
976
- * @example
977
- * ```typescript
978
- * const info = await client.tokens.getEncryptionInfo();
979
- * console.log('Algorithm:', info.algorithm);
980
- * console.log('Key derivation:', info.key_derivation);
981
- * console.log(info.javascript_example);
982
- * ```
983
- */
984
- async getEncryptionInfo() {
985
- return this.client.request("GET", "/tokens/encryption-info/");
986
- }
987
- };
988
-
989
- // src/client.ts
990
- var Resource = class {
991
- constructor(client, elementType) {
992
- this.client = client;
993
- this.elementType = elementType;
994
- }
995
- async list(options) {
996
- const response = await this.client.request("GET", `/${this.elementType}/`, { params: options });
997
- if (Array.isArray(response)) {
998
- return {
999
- count: response.length,
1000
- next: null,
1001
- previous: null,
1002
- results: response
1003
- };
1004
- }
1005
- return response;
1006
- }
1007
- async get(id) {
1008
- return this.client.request("GET", `/${this.elementType}/${id}/`);
1009
- }
1010
- async create(data) {
1011
- let body = OnlyWorldsClient.prepareRelations(data, this.elementType);
1012
- if (this.elementType === "pin") body = this.roundPinCoordinates(body);
1013
- return this.client.request("POST", `/${this.elementType}/`, { body });
1014
- }
1015
- async update(id, data) {
1016
- let body = OnlyWorldsClient.prepareRelations(data, this.elementType);
1017
- if (this.elementType === "pin") body = this.roundPinCoordinates(body);
1018
- return this.client.request("PATCH", `/${this.elementType}/${id}/`, { body });
1019
- }
1020
- async delete(id) {
1021
- return this.client.request("DELETE", `/${this.elementType}/${id}/`);
1022
- }
1023
- /**
1024
- * Round Pin coordinates to integers (API requirement)
1025
- * @private
1026
- */
1027
- roundPinCoordinates(data) {
1028
- const rounded = { ...data };
1029
- if (typeof rounded.x === "number") rounded.x = Math.round(rounded.x);
1030
- if (typeof rounded.y === "number") rounded.y = Math.round(rounded.y);
1031
- if (typeof rounded.z === "number") rounded.z = Math.round(rounded.z);
1032
- return rounded;
1033
- }
1034
- };
1035
- var WorldResource = class {
1036
- constructor(client) {
1037
- this.client = client;
1038
- }
1039
- /**
1040
- * Get the world associated with the current API key
1041
- * Returns the world directly (not wrapped in pagination)
1042
- */
1043
- async get() {
1044
- return this.client.request("GET", "/world/");
1045
- }
1046
- /**
1047
- * Update the current world
1048
- */
1049
- async update(data) {
1050
- return this.client.request("PATCH", "/world/", { body: data });
1051
- }
1052
- };
1053
- var OnlyWorldsClient = class {
1054
- constructor(config) {
1055
- this.baseUrl = config.baseUrl || "https://www.onlyworlds.com/api/worldapi";
1056
- this.headers = {
1057
- "Content-Type": "application/json",
1058
- "API-Key": config.apiKey,
1059
- "API-Pin": config.apiPin
1060
- };
1061
- this.worlds = new WorldResource(this);
1062
- this.tokens = new TokenResource(this);
1063
- this.abilities = new Resource(this, "ability");
1064
- this.characters = new Resource(this, "character");
1065
- this.collectives = new Resource(this, "collective");
1066
- this.constructs = new Resource(this, "construct");
1067
- this.creatures = new Resource(this, "creature");
1068
- this.events = new Resource(this, "event");
1069
- this.families = new Resource(this, "family");
1070
- this.institutions = new Resource(this, "institution");
1071
- this.languages = new Resource(this, "language");
1072
- this.laws = new Resource(this, "law");
1073
- this.locations = new Resource(this, "location");
1074
- this.maps = new Resource(this, "map");
1075
- this.markers = new Resource(this, "marker");
1076
- this.narratives = new Resource(this, "narrative");
1077
- this.objects = new Resource(this, "object");
1078
- this.phenomena = new Resource(this, "phenomenon");
1079
- this.pins = new Resource(this, "pin");
1080
- this.relations = new Resource(this, "relation");
1081
- this.species = new Resource(this, "species");
1082
- this.titles = new Resource(this, "title");
1083
- this.traits = new Resource(this, "trait");
1084
- this.zones = new Resource(this, "zone");
1085
- }
1086
- /**
1087
- * Make a request to the OnlyWorlds API
1088
- */
1089
- async request(method, path, options) {
1090
- const url = new URL(`${this.baseUrl}${path}`);
1091
- if (options?.params) {
1092
- Object.entries(options.params).forEach(([key, value]) => {
1093
- if (value !== void 0 && value !== null) {
1094
- url.searchParams.append(key, String(value));
1095
- }
1096
- });
1097
- }
1098
- const fetchOptions = {
1099
- method,
1100
- headers: this.headers
1101
- };
1102
- if (options?.body && ["POST", "PATCH", "PUT"].includes(method)) {
1103
- fetchOptions.body = JSON.stringify(options.body);
1104
- }
1105
- const response = await fetch(url.toString(), fetchOptions);
1106
- if (!response.ok) {
1107
- let errorMessage = `API Error ${response.status}`;
1108
- try {
1109
- const errorText = await response.text();
1110
- if (errorText) {
1111
- try {
1112
- const errorJson = JSON.parse(errorText);
1113
- if (Array.isArray(errorJson.detail)) {
1114
- const validationErrors = errorJson.detail.map((err) => {
1115
- const location = err.loc ? err.loc.join(".") : "unknown";
1116
- return `${location}: ${err.msg}`;
1117
- }).join("; ");
1118
- errorMessage += `: ${validationErrors}`;
1119
- } else {
1120
- errorMessage += `: ${errorJson.detail || errorJson.error || errorText}`;
1121
- }
1122
- } catch {
1123
- errorMessage += `: ${errorText}`;
1124
- }
1125
- }
1126
- } catch {
1127
- }
1128
- throw new Error(errorMessage);
1129
- }
1130
- if (response.status === 204) {
1131
- return void 0;
1132
- }
1133
- return response.json();
1134
- }
1135
- /**
1136
- * Helper to convert nested objects to _id/_ids format (legacy method)
1137
- * @deprecated Use prepareRelations instead - it's called automatically in create/update
1138
- */
1139
- static prepareInput(data) {
1140
- const result = { ...data };
1141
- for (const [key, value] of Object.entries(result)) {
1142
- if (value && typeof value === "object" && !Array.isArray(value) && "id" in value) {
1143
- delete result[key];
1144
- result[`${key}_id`] = value.id;
1145
- } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
1146
- delete result[key];
1147
- result[`${key}_ids`] = value.map((item) => item.id);
1148
- }
1149
- }
1150
- return result;
1151
- }
1152
- /**
1153
- * Convert relation fields to API format (_id/_ids suffix)
1154
- *
1155
- * The OnlyWorlds API expects:
1156
- * - single_link fields: fieldname_id (e.g., birthplace_id)
1157
- * - multi_link fields: fieldname_ids (e.g., species_ids)
1158
- *
1159
- * This method auto-converts based on FIELD_SCHEMA:
1160
- * - { species: ["id1", "id2"] } → { species_ids: ["id1", "id2"] }
1161
- * - { birthplace: "location-id" } → { birthplace_id: "location-id" }
1162
- * - { species: [{id: "id1", name: "X"}] } → { species_ids: ["id1"] }
1163
- *
1164
- * Called automatically by create() and update() methods.
1165
- */
1166
- static prepareRelations(data, elementType) {
1167
- const schema = FIELD_SCHEMA[elementType];
1168
- if (!schema) return data;
1169
- const result = { ...data };
1170
- for (const [key, value] of Object.entries(result)) {
1171
- const fieldDef = schema[key];
1172
- if (!fieldDef) continue;
1173
- if (fieldDef.type === "single_link") {
1174
- delete result[key];
1175
- if (value && typeof value === "object" && "id" in value) {
1176
- result[`${key}_id`] = value.id;
1177
- } else if (typeof value === "string" || value === null) {
1178
- result[`${key}_id`] = value;
1179
- }
1180
- } else if (fieldDef.type === "multi_link") {
1181
- delete result[key];
1182
- if (Array.isArray(value)) {
1183
- if (value.length > 0 && typeof value[0] === "object" && "id" in value[0]) {
1184
- result[`${key}_ids`] = value.map((item) => item.id);
1185
- } else {
1186
- result[`${key}_ids`] = value;
1187
- }
1188
- } else {
1189
- result[`${key}_ids`] = [];
1190
- }
1191
- }
1192
- }
1193
- return result;
1053
+ objects: { type: "multi_link", target: "object" },
1054
+ constructs: { type: "multi_link", target: "construct" },
1055
+ laws: { type: "multi_link", target: "law" },
1056
+ collectives: { type: "multi_link", target: "collective" },
1057
+ creatures: { type: "multi_link", target: "creature" },
1058
+ phenomena: { type: "multi_link", target: "phenomenon" },
1059
+ species: { type: "multi_link", target: "species" },
1060
+ languages: { type: "multi_link", target: "language" }
1061
+ },
1062
+ trait: {
1063
+ // Base fields (shared by all elements)
1064
+ name: { type: "text", required: true },
1065
+ description: { type: "text", required: false },
1066
+ supertype: { type: "text", required: false },
1067
+ subtype: { type: "text", required: false },
1068
+ image_url: { type: "text", required: false },
1069
+ // Qualitative
1070
+ social_effects: { type: "text" },
1071
+ physical_effects: { type: "text" },
1072
+ functional_effects: { type: "text" },
1073
+ personality_effects: { type: "text" },
1074
+ behaviour_effects: { type: "text" },
1075
+ // Quantitative
1076
+ charisma: { type: "integer" },
1077
+ coercion: { type: "integer" },
1078
+ competence: { type: "integer" },
1079
+ compassion: { type: "integer" },
1080
+ creativity: { type: "integer" },
1081
+ courage: { type: "integer" },
1082
+ // World
1083
+ significance: { type: "text" },
1084
+ anti_trait: { type: "single_link", target: "trait" },
1085
+ empowered_abilities: { type: "multi_link", target: "ability" }
1086
+ },
1087
+ zone: {
1088
+ // Base fields (shared by all elements)
1089
+ name: { type: "text", required: true },
1090
+ description: { type: "text", required: false },
1091
+ supertype: { type: "text", required: false },
1092
+ subtype: { type: "text", required: false },
1093
+ image_url: { type: "text", required: false },
1094
+ // Scope
1095
+ role: { type: "text" },
1096
+ start_date: { type: "integer" },
1097
+ end_date: { type: "integer" },
1098
+ phenomena: { type: "multi_link", target: "phenomenon" },
1099
+ linked_zones: { type: "multi_link", target: "zone" },
1100
+ // World
1101
+ context: { type: "text" },
1102
+ populations: { type: "multi_link", target: "collective" },
1103
+ titles: { type: "multi_link", target: "title" },
1104
+ principles: { type: "multi_link", target: "construct" }
1194
1105
  }
1195
1106
  };
1196
-
1197
- // src/token-types.ts
1198
- var GameTier = /* @__PURE__ */ ((GameTier2) => {
1199
- GameTier2["FREE"] = "free";
1200
- GameTier2["SILVER"] = "silver";
1201
- GameTier2["GOLD"] = "gold";
1202
- GameTier2["PLATINUM"] = "platinum";
1203
- GameTier2["DIAMOND"] = "diamond";
1204
- GameTier2["DELUXE"] = "deluxe";
1205
- return GameTier2;
1206
- })(GameTier || {});
1207
-
1208
- // src/icon-utils.ts
1209
1107
  var PLURAL_TO_SINGULAR = {
1210
- abilities: "ability" /* Ability */,
1211
- characters: "character" /* Character */,
1212
- collectives: "collective" /* Collective */,
1213
- constructs: "construct" /* Construct */,
1214
- creatures: "creature" /* Creature */,
1215
- events: "event" /* Event */,
1216
- families: "family" /* Family */,
1217
- institutions: "institution" /* Institution */,
1218
- languages: "language" /* Language */,
1219
- laws: "law" /* Law */,
1220
- locations: "location" /* Location */,
1221
- maps: "map" /* Map */,
1222
- markers: "marker" /* Marker */,
1223
- narratives: "narrative" /* Narrative */,
1224
- objects: "object" /* Object */,
1225
- phenomena: "phenomenon" /* Phenomenon */,
1108
+ abilities: "ability",
1109
+ characters: "character",
1110
+ collectives: "collective",
1111
+ constructs: "construct",
1112
+ creatures: "creature",
1113
+ events: "event",
1114
+ families: "family",
1115
+ institutions: "institution",
1116
+ languages: "language",
1117
+ laws: "law",
1118
+ locations: "location",
1119
+ maps: "map",
1120
+ markers: "marker",
1121
+ narratives: "narrative",
1122
+ objects: "object",
1123
+ phenomena: "phenomenon",
1226
1124
  // irregular plural
1227
- pins: "pin" /* Pin */,
1228
- relations: "relation" /* Relation */,
1229
- species: "species" /* Species */,
1125
+ pins: "pin",
1126
+ relations: "relation",
1127
+ species: "species",
1230
1128
  // same singular/plural
1231
- titles: "title" /* Title */,
1232
- traits: "trait" /* Trait */,
1233
- zones: "zone" /* Zone */
1129
+ titles: "title",
1130
+ traits: "trait",
1131
+ zones: "zone"
1234
1132
  };
1235
1133
  function getElementIcon(type) {
1236
1134
  const lower = type.toLowerCase();
1237
1135
  if (lower in PLURAL_TO_SINGULAR) {
1238
1136
  return ELEMENT_ICONS[PLURAL_TO_SINGULAR[lower]];
1239
1137
  }
1240
- const singular = Object.values(ElementType).find(
1241
- (et) => et.toLowerCase() === lower
1242
- );
1138
+ const singular = ELEMENT_TYPES.find((et) => et.toLowerCase() === lower);
1243
1139
  if (singular) {
1244
1140
  return ELEMENT_ICONS[singular];
1245
1141
  }
1246
1142
  return "help_outline";
1247
1143
  }
1248
-
1249
- // src/v2/errors.ts
1250
- var OwApiError = class extends Error {
1251
- constructor(status, code, message, docUrl, detail, type = null, param = null) {
1252
- super(message);
1253
- this.name = "OwApiError";
1254
- this.status = status;
1255
- this.code = code;
1256
- this.type = type;
1257
- this.param = param;
1258
- this.docUrl = docUrl;
1259
- this.detail = detail;
1260
- }
1261
- get isAuthError() {
1262
- return this.code === "invalid_credentials" || this.code === "key_revoked" || this.code === "world_gone";
1263
- }
1264
- /** 422s/400s name the offending param/field -- typos error loudly platform-wide. */
1265
- get isValidationError() {
1266
- return this.status === 422 || this.status === 400;
1267
- }
1268
- /** Same Idempotency-Key replayed with a different payload. */
1269
- get isIdempotencyConflict() {
1270
- return this.status === 409;
1271
- }
1272
- };
1273
- var OwNetworkError = class extends Error {
1274
- constructor(message, cause) {
1275
- super(message);
1276
- this.name = "OwNetworkError";
1277
- this.cause2 = cause;
1278
- }
1279
- };
1280
- function parseEnvelope(status, body) {
1281
- const env = body && typeof body === "object" ? body : {};
1282
- const nested = typeof env.error === "object" && env.error !== null ? env.error : void 0;
1283
- const code = env.code ?? nested?.code ?? (typeof env.error === "string" ? env.error : null) ?? null;
1284
- const type = env.type ?? nested?.type ?? null;
1285
- const param = env.param ?? nested?.param ?? null;
1286
- const docUrl = env.doc_url ?? nested?.doc_url ?? null;
1287
- const message = env.message ?? nested?.message ?? (typeof env.detail === "string" ? env.detail : void 0) ?? `OnlyWorlds API error ${status}${code ? ` (${code})` : ""}`;
1288
- return new OwApiError(status, code, message, docUrl, body, type, param);
1289
- }
1290
- async function errorFromResponse(res) {
1291
- let body = null;
1292
- let text = "";
1293
- try {
1294
- text = await res.text();
1295
- body = text ? JSON.parse(text) : null;
1296
- } catch {
1297
- body = text || null;
1298
- }
1299
- return parseEnvelope(res.status, body);
1300
- }
1301
-
1302
- // src/v2/keys.ts
1303
- var LEGACY_RE = /^[0-9]{10}$/;
1304
- function isDemoKey(key) {
1305
- return LEGACY_RE.test(key) && key >= "0000000000" && key <= "0000000009";
1306
- }
1307
- function detectKeyKind(key) {
1308
- if (key.startsWith("ow_w_")) return "write";
1309
- if (key.startsWith("ow_r_")) return "read";
1310
- if (key.startsWith("ow_a_")) return "account";
1311
- if (LEGACY_RE.test(key)) return "legacy";
1312
- return "unknown";
1313
- }
1314
- function kindCanWrite(kind) {
1315
- return kind === "write" || kind === "legacy";
1316
- }
1317
- function pinExpectation(kind) {
1318
- switch (kind) {
1319
- case "read":
1320
- return "never";
1321
- case "account":
1322
- return "never";
1323
- case "write":
1324
- return "optional";
1325
- case "legacy":
1326
- return "required-for-private-reads";
1327
- default:
1328
- return "optional";
1329
- }
1144
+ function getElementLabel(elementType) {
1145
+ return ELEMENT_LABELS[elementType];
1330
1146
  }
1331
1147
 
1332
- // src/v2/client.ts
1333
- var DEFAULT_BASE_URL = "https://www.onlyworlds.com/api/v2";
1334
- var DEFAULT_PAGE_SIZE = 100;
1335
- var DEFAULT_CHANGES_PAGE_SIZE = 100;
1336
- var OwV2Client = class {
1337
- constructor(config) {
1338
- if (!config.apiKey) throw new Error("OwV2Client: apiKey is required");
1339
- this.apiKey = config.apiKey;
1340
- this.apiPin = config.apiPin || void 0;
1341
- this.keyKind = detectKeyKind(config.apiKey);
1342
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1343
- this.pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
1344
- this.changesPageSize = config.changesPageSize ?? DEFAULT_CHANGES_PAGE_SIZE;
1345
- this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
1346
- if (typeof this.fetchImpl !== "function") {
1347
- throw new Error("OwV2Client: no fetch available -- supply config.fetch");
1348
- }
1349
- }
1350
- // -- Health & world meta -------------------------------------------------
1351
- /** GET /health -- unauthenticated liveness pulse. */
1352
- async health() {
1353
- return this.request("GET", "/health/", { auth: false });
1354
- }
1355
- /**
1356
- * GET /world -- world meta (name, calendar/time fields, public_read).
1357
- * GOTCHA (by server design): world-meta edits do NOT appear in /changes and
1358
- * do not bump change_seq. Poll getWorld().updated_at for meta freshness.
1359
- */
1360
- async getWorld() {
1361
- return this.request("GET", "/world/");
1362
- }
1363
- /** PATCH /world -- partial world-meta update. */
1364
- async patchWorld(partial) {
1365
- return this.request("PATCH", "/world/", { body: sanitizePayload(partial) });
1366
- }
1367
- // -- Element CRUD --------------------------------------------------------
1368
- /** GET /{type}/ -- one cursor page. */
1369
- async list(type, params = {}) {
1370
- const query = buildQuery({
1371
- limit: params.limit ?? this.pageSize,
1372
- cursor: params.cursor,
1373
- expand: params.expand?.join(","),
1374
- fields: params.fields?.join(","),
1375
- ...params.filter
1376
- });
1377
- return this.request("GET", `/${type}/`, { query });
1378
- }
1379
- /** Cursor-walk every page of a type. Politeness: uses config pageSize. */
1380
- async *listAll(type, params = {}) {
1381
- let cursor;
1382
- do {
1383
- const page = await this.list(type, { ...params, cursor });
1384
- for (const el of page.data) yield el;
1385
- cursor = page.has_more && page.next_cursor ? page.next_cursor : void 0;
1386
- } while (cursor);
1387
- }
1388
- /** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
1389
- async get(type, id, opts = {}) {
1390
- const query = buildQuery({ expand: opts.expand?.join(","), fields: opts.fields?.join(",") });
1391
- return this.request("GET", `/${type}/${id}/`, { query });
1148
+ // src/token-resource.ts
1149
+ var TokenResource = class {
1150
+ constructor(client) {
1151
+ this.client = client;
1392
1152
  }
1393
1153
  /**
1394
- * POST /{type}/ -- create. Mints an RFC-4122 UUID for element.id when the
1395
- * caller omits one (design ruling D29d) so a retry carrying the same
1396
- * Idempotency-Key is structurally safe. Callers MAY still supply their own id.
1154
+ * All token routes go through here: a 404 on /tokens/* almost always means
1155
+ * the SERVER predates the v2 token mount (keel >= 2026-07-23, e181689) —
1156
+ * not "user has no tokens". Annotate so the failure reads correctly.
1397
1157
  */
1398
- async create(type, element, opts = {}) {
1399
- const body = sanitizePayload(element);
1400
- if (body.id === void 0 || body.id === null || body.id === "") {
1401
- body.id = mintUuid();
1158
+ async req(method, path, opts) {
1159
+ try {
1160
+ return await this.client.request(method, path, opts);
1161
+ } catch (err) {
1162
+ if (err && typeof err === "object" && err.status === 404) {
1163
+ err.message += " [token routes require keel >= 2026-07-23 (e181689) on /api/v2 \u2014 a 404 here usually means an older server, not zero tokens]";
1164
+ }
1165
+ throw err;
1402
1166
  }
1403
- return this.request("POST", `/${type}/`, {
1404
- body,
1405
- idempotencyKey: opts.idempotencyKey
1406
- });
1407
- }
1408
- /** PUT /{type}/{id}/ -- upsert-by-client-id. The local-first write primitive. */
1409
- async upsert(type, id, element) {
1410
- return this.request("PUT", `/${type}/${id}/`, { body: sanitizePayload(element) });
1411
1167
  }
1412
1168
  /**
1413
- * PATCH /{type}/{id}/ -- partial update. DESTRUCTIVE on sent fields: arrays
1414
- * replace wholesale, omitted fields stay untouched. For link arrays prefer
1415
- * editLinks() -- atomic server-side merge, no read-before-write.
1169
+ * Get current token status for authenticated user
1170
+ *
1171
+ * Returns daily token allowance, usage, and availability.
1172
+ * Matches base-tool's checkStatus() pattern.
1173
+ *
1174
+ * @returns Current token status
1175
+ * @example
1176
+ * ```typescript
1177
+ * const status = await client.tokens.getStatus();
1178
+ * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
1179
+ * console.log(`Used today: ${status.tokens_used_today}`);
1180
+ * console.log(`Active sessions: ${status.sessions_active}`);
1181
+ * ```
1416
1182
  */
1417
- async patch(type, id, partial) {
1418
- return this.request("PATCH", `/${type}/${id}/`, { body: sanitizePayload(partial) });
1183
+ async getStatus() {
1184
+ return this.req("GET", "/tokens/status/");
1419
1185
  }
1420
1186
  /**
1421
- * DELETE /{type}/{id}/ -- idempotent (204 on absent). Server writes a
1422
- * tombstone AND scrubs the id from every other element's links in the same
1423
- * transaction -- no client-side unlink pass needed, ever.
1187
+ * Consume tokens for service usage
1188
+ *
1189
+ * Reports token consumption to track daily usage. Allows consumption even if
1190
+ * exceeds available tokens (tracks as debt), but warns via error field.
1191
+ * Matches base-tool's reportUsage() pattern.
1192
+ *
1193
+ * @param params - Token consumption parameters
1194
+ * @returns Consumption result with updated balance
1195
+ * @example
1196
+ * ```typescript
1197
+ * const result = await client.tokens.consume({
1198
+ * amount: 500,
1199
+ * service: 'worldbuilding_tool',
1200
+ * metadata: {
1201
+ * feature: 'character_generation',
1202
+ * model: 'gpt-4',
1203
+ * prompt_tokens: 300,
1204
+ * completion_tokens: 200
1205
+ * }
1206
+ * });
1207
+ *
1208
+ * if (result.error) {
1209
+ * console.warn('Token warning:', result.error);
1210
+ * }
1211
+ * console.log(`${result.tokens_remaining} tokens remaining`);
1212
+ * ```
1424
1213
  */
1425
- async delete(type, id) {
1426
- await this.request("DELETE", `/${type}/${id}/`, { allowEmpty: true });
1214
+ async consume(params) {
1215
+ return this.req("POST", "/tokens/consume/", {
1216
+ body: {
1217
+ amount: params.amount,
1218
+ service: params.service || "sdk_client",
1219
+ session_id: params.sessionId ?? null,
1220
+ metadata: params.metadata ?? null
1221
+ }
1222
+ });
1427
1223
  }
1428
1224
  /**
1429
- * POST /{type}/{id}/links/{field} with {add, remove} -- atomic link merge.
1430
- * Dedupes, tolerates already-present/already-absent ids. Returns the FULL
1431
- * updated element (fixture P5). Use this for all relationship editing; it
1432
- * retires the read-merge-PATCH dance.
1225
+ * Get encrypted OpenAI API key (advanced use case)
1226
+ *
1227
+ * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
1228
+ * Returns encrypted key that must be decrypted client-side using Fernet.
1229
+ *
1230
+ * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
1231
+ * including client-side decryption with the 'fernet' npm package.
1232
+ *
1233
+ * @returns Encrypted access key and session info
1234
+ * @throws Error if insufficient tokens (< 100)
1235
+ * @example
1236
+ * ```typescript
1237
+ * // Get encrypted key
1238
+ * const access = await client.tokens.getAccessKey();
1239
+ *
1240
+ * // Decrypt using fernet library (see base-tool for full example)
1241
+ * // 1. Derive key from world ID using SHA-256
1242
+ * // 2. Use 'fernet' npm package to decrypt
1243
+ * // 3. Use decrypted OpenAI key for direct API calls
1244
+ * // 4. Report usage with access.session_id
1245
+ *
1246
+ * console.log('Session:', access.session_id);
1247
+ * console.log('Expires:', access.expires_at);
1248
+ * ```
1433
1249
  */
1434
- async editLinks(type, id, field, edit) {
1435
- return this.request("POST", `/${type}/${id}/links/${field}`, { body: edit });
1250
+ async getAccessKey() {
1251
+ return this.req("GET", "/tokens/access-key/");
1436
1252
  }
1437
- // -- Bulk ----------------------------------------------------------------
1438
1253
  /**
1439
- * POST /bulk -- up to ~1000 items. Partial success by default (HTTP 200
1440
- * always; inspect per-slot numeric `status` + top-level `errors` flag);
1441
- * atomic:true for all-or-nothing. Link validation runs against batch U
1442
- * database -- send in any order, cycles included; no client topo-sort.
1443
- * Success slots echo server-authoritative timestamps: set your sync baseline
1444
- * from this response alone. When an idempotencyKey is replayed, the returned
1445
- * response carries wasReplay:true (read from the Idempotent-Replay header).
1254
+ * Revoke a specific token session
1255
+ *
1256
+ * Invalidates the session ID obtained from getAccessKey().
1257
+ * Use when cleaning up or on logout.
1258
+ *
1259
+ * @param sessionId - Session ID to revoke
1260
+ * @returns Revocation result
1261
+ * @example
1262
+ * ```typescript
1263
+ * await client.tokens.revokeSession('session-id-here');
1264
+ * ```
1446
1265
  */
1447
- async bulk(items, opts = {}) {
1448
- const body = {
1449
- items: items.map((it) => ({ type: it.type, element: sanitizePayload(it.element) })),
1450
- atomic: opts.atomic ?? false
1451
- };
1452
- return this.request("POST", "/bulk/", {
1453
- body,
1454
- idempotencyKey: opts.idempotencyKey,
1455
- replayAware: true
1456
- });
1266
+ async revokeSession(sessionId) {
1267
+ return this.req(
1268
+ "POST",
1269
+ `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
1270
+ );
1457
1271
  }
1458
- // -- Changes feed --------------------------------------------------------
1459
1272
  /**
1460
- * GET /changes -- one page of the world's ordered change feed.
1461
- * Cursor is OPAQUE and never expires: persist verbatim, never parse.
1462
- * Zero/absent cursor = full export (byte-aligned with the Folder Format).
1463
- * Rewind rule: if your persisted position is ahead of page.head, the server
1464
- * was restored -- re-baseline from cursor zero; do not assume caught-up.
1465
- * Citizenship: heaviest route on the platform; default page size is polite.
1273
+ * Revoke all active sessions (emergency use)
1274
+ *
1275
+ * Invalidates all token sessions for the authenticated user.
1276
+ * Use for security cleanup or when sessions are stuck.
1277
+ *
1278
+ * @returns Revocation result with count of revoked sessions
1279
+ * @example
1280
+ * ```typescript
1281
+ * const result = await client.tokens.revokeAllSessions();
1282
+ * console.log(`Revoked ${result.sessions_revoked} sessions`);
1283
+ * ```
1466
1284
  */
1467
- async changes(opts = {}) {
1468
- const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize });
1469
- return this.request("GET", "/changes/", { query });
1285
+ async revokeAllSessions() {
1286
+ return this.req("POST", "/tokens/revoke-all-sessions/");
1470
1287
  }
1471
1288
  /**
1472
- * Walk the feed from `since` (or from zero = full export) to the current
1473
- * tail, yielding ops in order. Returns the final cursor via the generator's
1474
- * return value; persist it for the next incremental pull.
1289
+ * Get public encryption info (no auth required)
1290
+ *
1291
+ * Returns algorithm details and example code for client-side decryption.
1292
+ * Public endpoint - can be called without authentication.
1293
+ *
1294
+ * @returns Encryption algorithm and implementation details
1295
+ * @example
1296
+ * ```typescript
1297
+ * const info = await client.tokens.getEncryptionInfo();
1298
+ * console.log('Algorithm:', info.algorithm);
1299
+ * console.log('Key derivation:', info.key_derivation);
1300
+ * console.log(info.javascript_example);
1301
+ * ```
1475
1302
  */
1476
- async *changesAll(since) {
1477
- let cursor = since;
1478
- let page;
1479
- do {
1480
- page = await this.changes({ since: cursor });
1481
- for (const op of page.changes) yield op;
1482
- cursor = page.cursor;
1483
- } while (page.has_more);
1484
- return { cursor: page.cursor, head: page.head };
1485
- }
1486
- // -- Core request machinery ----------------------------------------------
1487
- async request(method, path, opts = {}) {
1488
- const url = `${this.baseUrl}${path}${opts.query ? `?${opts.query}` : ""}`;
1489
- const headers = {};
1490
- if (opts.auth !== false) {
1491
- if (this.keyKind === "account") {
1492
- headers["Authorization"] = `Bearer ${this.apiKey}`;
1493
- } else {
1494
- headers["API-Key"] = this.apiKey;
1495
- if (this.apiPin) headers["API-Pin"] = this.apiPin;
1496
- }
1497
- }
1498
- if (opts.body !== void 0) headers["Content-Type"] = "application/json";
1499
- if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1500
- let res;
1501
- try {
1502
- res = await this.fetchImpl(url, {
1503
- method,
1504
- headers,
1505
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
1506
- });
1507
- } catch (cause) {
1508
- throw new OwNetworkError(`OnlyWorlds request failed: ${method} ${path}`, cause);
1509
- }
1510
- if (!res.ok) throw await errorFromResponse(res);
1511
- if (res.status === 204 || opts.allowEmpty) {
1512
- const text = await res.text();
1513
- return text ? JSON.parse(text) : null;
1514
- }
1515
- const parsed = await res.json();
1516
- if (opts.replayAware && parsed && typeof parsed === "object") {
1517
- parsed.wasReplay = readReplayHeader(res.headers);
1518
- }
1519
- return parsed;
1520
- }
1521
- };
1522
- var READ_ONLY_FIELDS = ["world", "type", "created_at", "updated_at", "change_seq"];
1523
- function sanitizePayload(payload) {
1524
- if (!payload || typeof payload !== "object") return payload;
1525
- const rest = { ...payload };
1526
- for (const field of READ_ONLY_FIELDS) delete rest[field];
1527
- return rest;
1528
- }
1529
- function readReplayHeader(headers) {
1530
- const v = headers.get("Idempotent-Replay");
1531
- return v != null && v.toLowerCase() === "true";
1532
- }
1533
- function mintUuid() {
1534
- const c = globalThis.crypto;
1535
- if (c && typeof c.randomUUID === "function") return c.randomUUID();
1536
- const bytes = new Uint8Array(16);
1537
- if (c && typeof c.getRandomValues === "function") {
1538
- c.getRandomValues(bytes);
1539
- } else {
1540
- for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
1541
- }
1542
- bytes[6] = bytes[6] & 15 | 64;
1543
- bytes[8] = bytes[8] & 63 | 128;
1544
- const hex = [];
1545
- for (let i = 0; i < 256; i++) hex.push((i + 256).toString(16).slice(1));
1546
- return hex[bytes[0]] + hex[bytes[1]] + hex[bytes[2]] + hex[bytes[3]] + "-" + hex[bytes[4]] + hex[bytes[5]] + "-" + hex[bytes[6]] + hex[bytes[7]] + "-" + hex[bytes[8]] + hex[bytes[9]] + "-" + hex[bytes[10]] + hex[bytes[11]] + hex[bytes[12]] + hex[bytes[13]] + hex[bytes[14]] + hex[bytes[15]];
1547
- }
1548
- function buildQuery(params) {
1549
- const q = new URLSearchParams();
1550
- for (const [k, v] of Object.entries(params)) {
1551
- if (v !== void 0 && v !== null && v !== "") q.set(k, String(v));
1303
+ async getEncryptionInfo() {
1304
+ return this.req("GET", "/tokens/encryption-info/");
1552
1305
  }
1553
- return q.toString();
1554
- }
1555
-
1556
- // src/v2/types.generated.ts
1557
- var ELEMENT_TYPES = ["ability", "character", "collective", "construct", "creature", "event", "family", "institution", "language", "law", "location", "map", "marker", "narrative", "object", "phenomenon", "pin", "relation", "species", "title", "trait", "zone"];
1558
- var ELEMENT_FAMILIES = {
1559
- ability: "abstract",
1560
- character: "agents",
1561
- collective: "agents",
1562
- construct: "world",
1563
- creature: "agents",
1564
- event: "temporal",
1565
- family: "agents",
1566
- institution: "agents",
1567
- language: "abstract",
1568
- law: "abstract",
1569
- location: "world",
1570
- map: "world",
1571
- marker: "world",
1572
- narrative: "temporal",
1573
- object: "world",
1574
- phenomenon: "temporal",
1575
- pin: "world",
1576
- relation: "temporal",
1577
- species: "agents",
1578
- title: "abstract",
1579
- trait: "abstract",
1580
- zone: "world"
1581
1306
  };
1582
1307
 
1583
- // src/v2/types.ts
1584
- var SPATIAL_TYPES = ["map", "pin", "marker", "zone"];
1585
-
1586
- // src/v2/palette.ts
1587
- var FAMILY_COLORS = {
1588
- agents: { light: "#2a78d6", dark: "#3987e5" },
1589
- world: { light: "#008300", dark: "#008300" },
1590
- abstract: { light: "#e87ba4", dark: "#d55181" },
1591
- temporal: { light: "#eda100", dark: "#c98500" }
1592
- };
1593
- function familyOf(type) {
1594
- return ELEMENT_FAMILIES[type];
1595
- }
1596
- function elementColor(type, mode = "dark") {
1597
- return FAMILY_COLORS[ELEMENT_FAMILIES[type]][mode];
1598
- }
1599
- var FAMILY_ORDER = ["agents", "world", "abstract", "temporal"];
1600
- // Annotate the CommonJS export names for ESM import in node:
1601
- 0 && (module.exports = {
1308
+ // src/token-types.ts
1309
+ var GameTier = /* @__PURE__ */ ((GameTier2) => {
1310
+ GameTier2["FREE"] = "free";
1311
+ GameTier2["SILVER"] = "silver";
1312
+ GameTier2["GOLD"] = "gold";
1313
+ GameTier2["PLATINUM"] = "platinum";
1314
+ GameTier2["DIAMOND"] = "diamond";
1315
+ GameTier2["DELUXE"] = "deluxe";
1316
+ return GameTier2;
1317
+ })(GameTier || {});
1318
+ export {
1602
1319
  ELEMENT_FAMILIES,
1603
1320
  ELEMENT_ICONS,
1604
1321
  ELEMENT_LABELS,
1605
1322
  ELEMENT_SECTIONS,
1606
1323
  ELEMENT_TYPES,
1607
- ElementType,
1608
1324
  FAMILY_COLORS,
1609
1325
  FAMILY_ORDER,
1610
1326
  FIELD_SCHEMA,
1611
1327
  GameTier,
1612
1328
  ONLYWORLDS_VERSION,
1613
- OnlyWorldsClient,
1614
1329
  OwApiError,
1615
1330
  OwNetworkError,
1616
1331
  OwV2Client,
1617
1332
  SPATIAL_TYPES,
1618
- createAnyElementId,
1619
- createElementId,
1620
- createElementIds,
1333
+ TokenResource,
1621
1334
  detectKeyKind,
1622
1335
  elementColor,
1623
1336
  errorFromResponse,
1624
1337
  familyOf,
1625
1338
  getElementIcon,
1626
1339
  getElementLabel,
1627
- getElementSections,
1628
1340
  isDemoKey,
1629
1341
  kindCanWrite,
1630
- parseEnvelope,
1342
+ parseErrorEnvelope,
1631
1343
  pinExpectation
1632
- });
1344
+ };