@onlyworlds/sdk 3.1.0 → 4.0.0-alpha.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/dist/index.js CHANGED
@@ -1,117 +1,373 @@
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 parseEnvelope(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 parseEnvelope(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
+ }
280
+ };
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;
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 ELEMENT_FAMILIES = {
318
+ ability: "abstract",
319
+ character: "agents",
320
+ collective: "agents",
321
+ construct: "world",
322
+ creature: "agents",
323
+ event: "temporal",
324
+ family: "agents",
325
+ institution: "agents",
326
+ language: "abstract",
327
+ law: "abstract",
328
+ location: "world",
329
+ map: "world",
330
+ marker: "world",
331
+ narrative: "temporal",
332
+ object: "world",
333
+ phenomenon: "temporal",
334
+ pin: "world",
335
+ relation: "temporal",
336
+ species: "agents",
337
+ title: "abstract",
338
+ trait: "abstract",
339
+ zone: "world"
340
+ };
341
+ var ELEMENT_ICONS = {
342
+ ability: "auto_fix_normal",
343
+ character: "person",
344
+ collective: "groups_3",
345
+ construct: "api",
346
+ creature: "bug_report",
347
+ event: "saved_search",
348
+ family: "supervisor_account",
349
+ institution: "business",
350
+ language: "edit_road",
351
+ law: "gpp_bad",
352
+ location: "castle",
353
+ map: "map",
354
+ marker: "place",
355
+ narrative: "menu_book",
356
+ object: "webhook",
357
+ phenomenon: "thunderstorm",
358
+ pin: "push_pin",
359
+ relation: "link",
360
+ species: "crib",
361
+ title: "military_tech",
362
+ trait: "flaky",
363
+ zone: "architecture"
105
364
  };
106
- function getElementLabel(elementType) {
107
- return ELEMENT_LABELS[elementType];
108
- }
109
365
  var ELEMENT_SECTIONS = {
110
- ["ability" /* Ability */]: [
366
+ ability: [
111
367
  { name: "Mechanics", order: 1, fields: ["activation", "duration", "potency", "range", "effects", "challenges", "talents", "requisites"] },
112
368
  { name: "World", order: 2, fields: ["prevalence", "tradition", "source", "locus", "instruments", "systems"] }
113
369
  ],
114
- ["character" /* Character */]: [
370
+ character: [
115
371
  { name: "Constitution", order: 1, fields: ["physicality", "mentality", "height", "weight", "species", "traits", "abilities"] },
116
372
  { name: "Origins", order: 2, fields: ["background", "motivations", "birth_date", "birthplace", "languages"] },
117
373
  { name: "World", order: 3, fields: ["reputation", "location", "objects", "institutions"] },
@@ -119,43 +375,43 @@ var ELEMENT_SECTIONS = {
119
375
  { name: "Social", order: 5, fields: ["family", "friends", "rivals"] },
120
376
  { name: "TTRPG", order: 6, fields: ["level", "hit_points", "STR", "DEX", "CON", "INT", "WIS", "CHA"] }
121
377
  ],
122
- ["collective" /* Collective */]: [
378
+ collective: [
123
379
  { name: "Formation", order: 1, fields: ["composition", "count", "formation_date", "operator", "equipment"] },
124
380
  { name: "Dynamics", order: 2, fields: ["activity", "disposition", "state", "abilities", "symbolism"] },
125
381
  { name: "World", order: 3, fields: ["species", "characters", "creatures", "phenomena"] }
126
382
  ],
127
- ["construct" /* Construct */]: [
383
+ construct: [
128
384
  { name: "Nature", order: 1, fields: ["rationale", "history", "status", "reach", "start_date", "end_date", "founder", "custodian"] },
129
385
  { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs", "events", "narratives"] }
130
386
  ],
131
- ["creature" /* Creature */]: [
387
+ creature: [
132
388
  { name: "Biology", order: 1, fields: ["appearance", "weight", "height", "species"] },
133
- { name: "Behaviour", order: 2, fields: ["habits", "demeanor", "traits", "abilities", "languages"] },
389
+ { name: "Behavior", order: 2, fields: ["habits", "demeanor", "traits", "abilities", "languages"] },
134
390
  { name: "World", order: 3, fields: ["status", "birth_date", "location", "zone"] },
135
391
  { name: "TTRPG", order: 4, fields: ["challenge_rating", "hit_points", "armor_class", "speed", "actions"] }
136
392
  ],
137
- ["event" /* Event */]: [
393
+ event: [
138
394
  { name: "Nature", order: 1, fields: ["history", "challenges", "consequences", "start_date", "end_date", "triggers"] },
139
395
  { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs"] }
140
396
  ],
141
- ["family" /* Family */]: [
397
+ family: [
142
398
  { name: "Identity", order: 1, fields: ["spirit", "history", "traditions", "traits", "abilities", "languages", "ancestors"] },
143
399
  { name: "World", order: 2, fields: ["reputation", "estates", "governs", "heirlooms", "creatures"] }
144
400
  ],
145
- ["institution" /* Institution */]: [
401
+ institution: [
146
402
  { name: "Foundation", order: 1, fields: ["doctrine", "founding_date", "parent_institution"] },
147
403
  { name: "Claims", order: 2, fields: ["zones", "objects", "creatures"] },
148
404
  { name: "World", order: 3, fields: ["status", "allies", "adversaries", "constructs"] }
149
405
  ],
150
- ["language" /* Language */]: [
406
+ language: [
151
407
  { name: "Structure", order: 1, fields: ["phonology", "grammar", "lexicon", "writing", "classification"] },
152
408
  { name: "World", order: 2, fields: ["status", "spread", "dialects"] }
153
409
  ],
154
- ["law" /* Law */]: [
410
+ law: [
155
411
  { name: "Code", order: 1, fields: ["declaration", "purpose", "date", "parent_law", "penalties"] },
156
412
  { name: "World", order: 2, fields: ["author", "locations", "zones", "prohibitions", "adjudicators", "enforcers"] }
157
413
  ],
158
- ["location" /* Location */]: [
414
+ location: [
159
415
  { name: "Setting", order: 1, fields: ["form", "function", "founding_date", "parent_location", "populations"] },
160
416
  { name: "Politics", order: 2, fields: ["political_climate", "primary_power", "governing_title", "secondary_powers", "zone", "rival", "partner"] },
161
417
  { name: "World", order: 3, fields: ["customs", "founders", "cults", "delicacies"] },
@@ -164,78 +420,95 @@ var ELEMENT_SECTIONS = {
164
420
  { name: "Construction", order: 6, fields: ["architecture", "buildings", "building_methods"] },
165
421
  { name: "Defense", order: 7, fields: ["defensibility", "elevation", "fighters", "defensive_objects"] }
166
422
  ],
167
- ["map" /* Map */]: [
423
+ map: [
168
424
  { name: "Details", order: 1, fields: ["background_color", "hierarchy", "width", "height", "depth", "parent_map", "location"] }
169
425
  ],
170
- ["marker" /* Marker */]: [
426
+ marker: [
171
427
  { name: "Details", order: 1, fields: ["map", "zone", "x", "y", "z", "order"] }
172
428
  ],
173
- ["narrative" /* Narrative */]: [
429
+ narrative: [
174
430
  { name: "Context", order: 1, fields: ["story", "consequences", "start_date", "end_date", "order", "parent_narrative", "protagonist", "antagonist", "narrator", "conservator"] },
175
431
  { name: "Involves", order: 2, fields: ["events", "characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "relations", "titles", "constructs", "laws"] }
176
432
  ],
177
- ["object" /* Object */]: [
433
+ object: [
178
434
  { name: "Form", order: 1, fields: ["aesthetics", "weight", "amount", "parent_object", "materials", "technology"] },
179
435
  { name: "Function", order: 2, fields: ["utility", "effects", "abilities", "consumes"] },
180
436
  { name: "World", order: 3, fields: ["origins", "location", "language", "affinities"] }
181
437
  ],
182
- ["phenomenon" /* Phenomenon */]: [
438
+ phenomenon: [
183
439
  { name: "Mechanics", order: 1, fields: ["expression", "effects", "duration", "catalysts", "empowerments"] },
184
440
  { name: "World", order: 2, fields: ["mythology", "system", "triggers", "wielders", "environments"] }
185
441
  ],
186
- ["pin" /* Pin */]: [
187
- { name: "Details", order: 1, fields: ["map", "element_type", "element_id", "element", "x", "y", "z"] }
442
+ pin: [
443
+ { name: "Details", order: 1, fields: ["map", "element", "x", "y", "z"] }
188
444
  ],
189
- ["relation" /* Relation */]: [
445
+ relation: [
190
446
  { 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"] }
447
+ { name: "Involves", order: 2, fields: ["characters", "objects", "locations", "species", "creatures", "institutions", "traits", "collectives", "zones", "abilities", "phenomena", "languages", "families", "titles", "constructs", "events", "narratives"] }
192
448
  ],
193
- ["species" /* Species */]: [
449
+ species: [
194
450
  { name: "Biology", order: 1, fields: ["appearance", "life_span", "weight", "nourishment", "reproduction", "adaptations"] },
195
451
  { name: "Psychology", order: 2, fields: ["instincts", "sociality", "temperament", "communication", "aggression", "traits"] },
196
452
  { name: "World", order: 3, fields: ["role", "parent_species", "locations", "zones", "affinities"] }
197
453
  ],
198
- ["title" /* Title */]: [
454
+ title: [
199
455
  { name: "Mandate", order: 1, fields: ["authority", "eligibility", "grant_date", "revoke_date", "issuer", "body", "superior_title", "holders", "symbols"] },
200
456
  { name: "World", order: 2, fields: ["status", "history", "characters", "institutions", "families", "zones", "locations", "objects", "constructs", "laws", "collectives", "creatures", "phenomena", "species", "languages"] }
201
457
  ],
202
- ["trait" /* Trait */]: [
458
+ trait: [
203
459
  { name: "Qualitative", order: 1, fields: ["social_effects", "physical_effects", "functional_effects", "personality_effects", "behaviour_effects"] },
204
460
  { name: "Quantitative", order: 2, fields: ["charisma", "coercion", "competence", "compassion", "creativity", "courage"] },
205
461
  { name: "World", order: 3, fields: ["significance", "anti_trait", "empowered_abilities"] }
206
462
  ],
207
- ["zone" /* Zone */]: [
463
+ zone: [
208
464
  { name: "Scope", order: 1, fields: ["role", "start_date", "end_date", "phenomena", "linked_zones"] },
209
465
  { name: "World", order: 2, fields: ["context", "populations", "titles", "principles"] }
210
466
  ]
211
467
  };
212
- function getElementSections(elementType) {
213
- return ELEMENT_SECTIONS[elementType] || [];
468
+
469
+ // src/v2/types.ts
470
+ var SPATIAL_TYPES = ["map", "pin", "marker", "zone"];
471
+
472
+ // src/v2/palette.ts
473
+ var FAMILY_COLORS = {
474
+ agents: { light: "#2a78d6", dark: "#3987e5" },
475
+ world: { light: "#008300", dark: "#008300" },
476
+ abstract: { light: "#e87ba4", dark: "#d55181" },
477
+ temporal: { light: "#eda100", dark: "#c98500" }
478
+ };
479
+ function familyOf(type) {
480
+ return ELEMENT_FAMILIES[type];
481
+ }
482
+ function elementColor(type, mode = "dark") {
483
+ return FAMILY_COLORS[ELEMENT_FAMILIES[type]][mode];
214
484
  }
485
+ var FAMILY_ORDER = ["agents", "world", "abstract", "temporal"];
486
+
487
+ // src/v2/constants.ts
215
488
  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"
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" },
@@ -787,846 +1060,270 @@ var FIELD_SCHEMA = {
787
1060
  languages: { type: "multi_link", target: "language" }
788
1061
  },
789
1062
  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;
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 });
1392
- }
1393
- /**
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.
1397
- */
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();
1402
- }
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) });
1148
+ // src/token-resource.ts
1149
+ var TokenResource = class {
1150
+ constructor(client) {
1151
+ this.client = client;
1411
1152
  }
1412
1153
  /**
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.
1154
+ * Get current token status for authenticated user
1155
+ *
1156
+ * Returns daily token allowance, usage, and availability.
1157
+ * Matches base-tool's checkStatus() pattern.
1158
+ *
1159
+ * @returns Current token status
1160
+ * @example
1161
+ * ```typescript
1162
+ * const status = await client.tokens.getStatus();
1163
+ * console.log(`Available: ${status.tokens_available_today}/${status.token_rating}`);
1164
+ * console.log(`Used today: ${status.tokens_used_today}`);
1165
+ * console.log(`Active sessions: ${status.sessions_active}`);
1166
+ * ```
1416
1167
  */
1417
- async patch(type, id, partial) {
1418
- return this.request("PATCH", `/${type}/${id}/`, { body: sanitizePayload(partial) });
1168
+ async getStatus() {
1169
+ return this.client.request("GET", "/tokens/status/");
1419
1170
  }
1420
1171
  /**
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.
1172
+ * Consume tokens for service usage
1173
+ *
1174
+ * Reports token consumption to track daily usage. Allows consumption even if
1175
+ * exceeds available tokens (tracks as debt), but warns via error field.
1176
+ * Matches base-tool's reportUsage() pattern.
1177
+ *
1178
+ * @param params - Token consumption parameters
1179
+ * @returns Consumption result with updated balance
1180
+ * @example
1181
+ * ```typescript
1182
+ * const result = await client.tokens.consume({
1183
+ * amount: 500,
1184
+ * service: 'worldbuilding_tool',
1185
+ * metadata: {
1186
+ * feature: 'character_generation',
1187
+ * model: 'gpt-4',
1188
+ * prompt_tokens: 300,
1189
+ * completion_tokens: 200
1190
+ * }
1191
+ * });
1192
+ *
1193
+ * if (result.error) {
1194
+ * console.warn('Token warning:', result.error);
1195
+ * }
1196
+ * console.log(`${result.tokens_remaining} tokens remaining`);
1197
+ * ```
1424
1198
  */
1425
- async delete(type, id) {
1426
- await this.request("DELETE", `/${type}/${id}/`, { allowEmpty: true });
1199
+ async consume(params) {
1200
+ return this.client.request("POST", "/tokens/consume/", {
1201
+ body: {
1202
+ amount: params.amount,
1203
+ service: params.service || "sdk_client",
1204
+ session_id: params.sessionId ?? null,
1205
+ metadata: params.metadata ?? null
1206
+ }
1207
+ });
1427
1208
  }
1428
1209
  /**
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.
1210
+ * Get encrypted OpenAI API key (advanced use case)
1211
+ *
1212
+ * Requires minimum 100 tokens available. Creates a 1-hour session for tracking.
1213
+ * Returns encrypted key that must be decrypted client-side using Fernet.
1214
+ *
1215
+ * See base-tool/src/llm/token-service.ts:99-155 for full implementation example
1216
+ * including client-side decryption with the 'fernet' npm package.
1217
+ *
1218
+ * @returns Encrypted access key and session info
1219
+ * @throws Error if insufficient tokens (< 100)
1220
+ * @example
1221
+ * ```typescript
1222
+ * // Get encrypted key
1223
+ * const access = await client.tokens.getAccessKey();
1224
+ *
1225
+ * // Decrypt using fernet library (see base-tool for full example)
1226
+ * // 1. Derive key from world ID using SHA-256
1227
+ * // 2. Use 'fernet' npm package to decrypt
1228
+ * // 3. Use decrypted OpenAI key for direct API calls
1229
+ * // 4. Report usage with access.session_id
1230
+ *
1231
+ * console.log('Session:', access.session_id);
1232
+ * console.log('Expires:', access.expires_at);
1233
+ * ```
1433
1234
  */
1434
- async editLinks(type, id, field, edit) {
1435
- return this.request("POST", `/${type}/${id}/links/${field}`, { body: edit });
1235
+ async getAccessKey() {
1236
+ return this.client.request("GET", "/tokens/access-key/");
1436
1237
  }
1437
- // -- Bulk ----------------------------------------------------------------
1438
1238
  /**
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).
1239
+ * Revoke a specific token session
1240
+ *
1241
+ * Invalidates the session ID obtained from getAccessKey().
1242
+ * Use when cleaning up or on logout.
1243
+ *
1244
+ * @param sessionId - Session ID to revoke
1245
+ * @returns Revocation result
1246
+ * @example
1247
+ * ```typescript
1248
+ * await client.tokens.revokeSession('session-id-here');
1249
+ * ```
1446
1250
  */
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
- });
1251
+ async revokeSession(sessionId) {
1252
+ return this.client.request(
1253
+ "POST",
1254
+ `/tokens/revoke-session/?session_id=${encodeURIComponent(sessionId)}`
1255
+ );
1457
1256
  }
1458
- // -- Changes feed --------------------------------------------------------
1459
1257
  /**
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.
1258
+ * Revoke all active sessions (emergency use)
1259
+ *
1260
+ * Invalidates all token sessions for the authenticated user.
1261
+ * Use for security cleanup or when sessions are stuck.
1262
+ *
1263
+ * @returns Revocation result with count of revoked sessions
1264
+ * @example
1265
+ * ```typescript
1266
+ * const result = await client.tokens.revokeAllSessions();
1267
+ * console.log(`Revoked ${result.sessions_revoked} sessions`);
1268
+ * ```
1466
1269
  */
1467
- async changes(opts = {}) {
1468
- const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize });
1469
- return this.request("GET", "/changes/", { query });
1270
+ async revokeAllSessions() {
1271
+ return this.client.request("POST", "/tokens/revoke-all-sessions/");
1470
1272
  }
1471
1273
  /**
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.
1274
+ * Get public encryption info (no auth required)
1275
+ *
1276
+ * Returns algorithm details and example code for client-side decryption.
1277
+ * Public endpoint - can be called without authentication.
1278
+ *
1279
+ * @returns Encryption algorithm and implementation details
1280
+ * @example
1281
+ * ```typescript
1282
+ * const info = await client.tokens.getEncryptionInfo();
1283
+ * console.log('Algorithm:', info.algorithm);
1284
+ * console.log('Key derivation:', info.key_derivation);
1285
+ * console.log(info.javascript_example);
1286
+ * ```
1475
1287
  */
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));
1288
+ async getEncryptionInfo() {
1289
+ return this.client.request("GET", "/tokens/encryption-info/");
1552
1290
  }
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
1291
  };
1582
1292
 
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 = {
1293
+ // src/token-types.ts
1294
+ var GameTier = /* @__PURE__ */ ((GameTier2) => {
1295
+ GameTier2["FREE"] = "free";
1296
+ GameTier2["SILVER"] = "silver";
1297
+ GameTier2["GOLD"] = "gold";
1298
+ GameTier2["PLATINUM"] = "platinum";
1299
+ GameTier2["DIAMOND"] = "diamond";
1300
+ GameTier2["DELUXE"] = "deluxe";
1301
+ return GameTier2;
1302
+ })(GameTier || {});
1303
+ export {
1602
1304
  ELEMENT_FAMILIES,
1603
1305
  ELEMENT_ICONS,
1604
1306
  ELEMENT_LABELS,
1605
1307
  ELEMENT_SECTIONS,
1606
1308
  ELEMENT_TYPES,
1607
- ElementType,
1608
1309
  FAMILY_COLORS,
1609
1310
  FAMILY_ORDER,
1610
1311
  FIELD_SCHEMA,
1611
1312
  GameTier,
1612
1313
  ONLYWORLDS_VERSION,
1613
- OnlyWorldsClient,
1614
1314
  OwApiError,
1615
1315
  OwNetworkError,
1616
1316
  OwV2Client,
1617
1317
  SPATIAL_TYPES,
1618
- createAnyElementId,
1619
- createElementId,
1620
- createElementIds,
1318
+ TokenResource,
1621
1319
  detectKeyKind,
1622
1320
  elementColor,
1623
1321
  errorFromResponse,
1624
1322
  familyOf,
1625
1323
  getElementIcon,
1626
1324
  getElementLabel,
1627
- getElementSections,
1628
1325
  isDemoKey,
1629
1326
  kindCanWrite,
1630
1327
  parseEnvelope,
1631
1328
  pinExpectation
1632
- });
1329
+ };