@onlyworlds/sdk 2.2.2 → 3.0.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
@@ -23,17 +23,28 @@ __export(index_exports, {
23
23
  ELEMENT_ICONS: () => ELEMENT_ICONS,
24
24
  ELEMENT_LABELS: () => ELEMENT_LABELS,
25
25
  ELEMENT_SECTIONS: () => ELEMENT_SECTIONS,
26
+ ELEMENT_TYPES: () => ELEMENT_TYPES,
26
27
  ElementType: () => ElementType,
27
28
  FIELD_SCHEMA: () => FIELD_SCHEMA,
28
29
  GameTier: () => GameTier,
29
30
  ONLYWORLDS_VERSION: () => ONLYWORLDS_VERSION,
30
31
  OnlyWorldsClient: () => OnlyWorldsClient,
32
+ OwApiError: () => OwApiError,
33
+ OwNetworkError: () => OwNetworkError,
34
+ OwV2Client: () => OwV2Client,
35
+ SPATIAL_TYPES: () => SPATIAL_TYPES,
31
36
  createAnyElementId: () => createAnyElementId,
32
37
  createElementId: () => createElementId,
33
38
  createElementIds: () => createElementIds,
39
+ detectKeyKind: () => detectKeyKind,
40
+ errorFromResponse: () => errorFromResponse,
34
41
  getElementIcon: () => getElementIcon,
35
42
  getElementLabel: () => getElementLabel,
36
- getElementSections: () => getElementSections
43
+ getElementSections: () => getElementSections,
44
+ isDemoKey: () => isDemoKey,
45
+ kindCanWrite: () => kindCanWrite,
46
+ parseEnvelope: () => parseEnvelope,
47
+ pinExpectation: () => pinExpectation
37
48
  });
38
49
  module.exports = __toCommonJS(index_exports);
39
50
 
@@ -568,12 +579,12 @@ var FIELD_SCHEMA = {
568
579
  subtype: { type: "text", required: false },
569
580
  image_url: { type: "text", required: false },
570
581
  // Details
571
- map: { type: "single_link", target: "map" },
572
- zone: { type: "single_link", target: "zone" },
573
- x: { type: "number" },
574
- y: { type: "number" },
582
+ map: { type: "single_link", target: "map", required: true },
583
+ zone: { type: "single_link", target: "zone", required: true },
584
+ x: { type: "number", required: true },
585
+ y: { type: "number", required: true },
575
586
  z: { type: "number" },
576
- order: { type: "number" }
587
+ order: { type: "number", required: true }
577
588
  },
578
589
  narrative: {
579
590
  // Base fields (shared by all elements)
@@ -666,13 +677,13 @@ var FIELD_SCHEMA = {
666
677
  subtype: { type: "text", required: false },
667
678
  image_url: { type: "text", required: false },
668
679
  // Details
669
- map: { type: "single_link", target: "map" },
670
- element_type: { type: "text" },
671
- // ElementType enum value
672
- element_id: { type: "single_link", target: "any" },
680
+ map: { type: "single_link", target: "map", required: true },
681
+ element_type: { type: "text", required: true },
682
+ // ElementType enum value; YAML 'element' generic-link is split into _type + _id
683
+ element_id: { type: "single_link", target: "any", required: true },
673
684
  // Can reference any element
674
- x: { type: "number" },
675
- y: { type: "number" },
685
+ x: { type: "number", required: true },
686
+ y: { type: "number", required: true },
676
687
  z: { type: "number" }
677
688
  },
678
689
  relation: {
@@ -1229,20 +1240,344 @@ function getElementIcon(type) {
1229
1240
  }
1230
1241
  return "help_outline";
1231
1242
  }
1243
+
1244
+ // src/v2/errors.ts
1245
+ var OwApiError = class extends Error {
1246
+ constructor(status, code, message, docUrl, detail, type = null, param = null) {
1247
+ super(message);
1248
+ this.name = "OwApiError";
1249
+ this.status = status;
1250
+ this.code = code;
1251
+ this.type = type;
1252
+ this.param = param;
1253
+ this.docUrl = docUrl;
1254
+ this.detail = detail;
1255
+ }
1256
+ get isAuthError() {
1257
+ return this.code === "invalid_credentials" || this.code === "key_revoked" || this.code === "world_gone";
1258
+ }
1259
+ /** 422s/400s name the offending param/field -- typos error loudly platform-wide. */
1260
+ get isValidationError() {
1261
+ return this.status === 422 || this.status === 400;
1262
+ }
1263
+ /** Same Idempotency-Key replayed with a different payload. */
1264
+ get isIdempotencyConflict() {
1265
+ return this.status === 409;
1266
+ }
1267
+ };
1268
+ var OwNetworkError = class extends Error {
1269
+ constructor(message, cause) {
1270
+ super(message);
1271
+ this.name = "OwNetworkError";
1272
+ this.cause2 = cause;
1273
+ }
1274
+ };
1275
+ function parseEnvelope(status, body) {
1276
+ const env = body && typeof body === "object" ? body : {};
1277
+ const nested = typeof env.error === "object" && env.error !== null ? env.error : void 0;
1278
+ const code = env.code ?? nested?.code ?? (typeof env.error === "string" ? env.error : null) ?? null;
1279
+ const type = env.type ?? nested?.type ?? null;
1280
+ const param = env.param ?? nested?.param ?? null;
1281
+ const docUrl = env.doc_url ?? nested?.doc_url ?? null;
1282
+ const message = env.message ?? nested?.message ?? (typeof env.detail === "string" ? env.detail : void 0) ?? `OnlyWorlds API error ${status}${code ? ` (${code})` : ""}`;
1283
+ return new OwApiError(status, code, message, docUrl, body, type, param);
1284
+ }
1285
+ async function errorFromResponse(res) {
1286
+ let body = null;
1287
+ let text = "";
1288
+ try {
1289
+ text = await res.text();
1290
+ body = text ? JSON.parse(text) : null;
1291
+ } catch {
1292
+ body = text || null;
1293
+ }
1294
+ return parseEnvelope(res.status, body);
1295
+ }
1296
+
1297
+ // src/v2/keys.ts
1298
+ var LEGACY_RE = /^[0-9]{10}$/;
1299
+ function isDemoKey(key) {
1300
+ return LEGACY_RE.test(key) && key >= "0000000000" && key <= "0000000009";
1301
+ }
1302
+ function detectKeyKind(key) {
1303
+ if (key.startsWith("ow_w_")) return "write";
1304
+ if (key.startsWith("ow_r_")) return "read";
1305
+ if (key.startsWith("ow_a_")) return "account";
1306
+ if (LEGACY_RE.test(key)) return "legacy";
1307
+ return "unknown";
1308
+ }
1309
+ function kindCanWrite(kind) {
1310
+ return kind === "write" || kind === "legacy";
1311
+ }
1312
+ function pinExpectation(kind) {
1313
+ switch (kind) {
1314
+ case "read":
1315
+ return "never";
1316
+ case "account":
1317
+ return "never";
1318
+ case "write":
1319
+ return "optional";
1320
+ case "legacy":
1321
+ return "required-for-private-reads";
1322
+ default:
1323
+ return "optional";
1324
+ }
1325
+ }
1326
+
1327
+ // src/v2/client.ts
1328
+ var DEFAULT_BASE_URL = "https://www.onlyworlds.com/api/v2";
1329
+ var DEFAULT_PAGE_SIZE = 100;
1330
+ var DEFAULT_CHANGES_PAGE_SIZE = 100;
1331
+ var OwV2Client = class {
1332
+ constructor(config) {
1333
+ if (!config.apiKey) throw new Error("OwV2Client: apiKey is required");
1334
+ this.apiKey = config.apiKey;
1335
+ this.apiPin = config.apiPin || void 0;
1336
+ this.keyKind = detectKeyKind(config.apiKey);
1337
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1338
+ this.pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
1339
+ this.changesPageSize = config.changesPageSize ?? DEFAULT_CHANGES_PAGE_SIZE;
1340
+ this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
1341
+ if (typeof this.fetchImpl !== "function") {
1342
+ throw new Error("OwV2Client: no fetch available -- supply config.fetch");
1343
+ }
1344
+ }
1345
+ // -- Health & world meta -------------------------------------------------
1346
+ /** GET /health -- unauthenticated liveness pulse. */
1347
+ async health() {
1348
+ return this.request("GET", "/health/", { auth: false });
1349
+ }
1350
+ /**
1351
+ * GET /world -- world meta (name, calendar/time fields, public_read).
1352
+ * GOTCHA (by server design): world-meta edits do NOT appear in /changes and
1353
+ * do not bump change_seq. Poll getWorld().updated_at for meta freshness.
1354
+ */
1355
+ async getWorld() {
1356
+ return this.request("GET", "/world/");
1357
+ }
1358
+ /** PATCH /world -- partial world-meta update. */
1359
+ async patchWorld(partial) {
1360
+ return this.request("PATCH", "/world/", { body: sanitizePayload(partial) });
1361
+ }
1362
+ // -- Element CRUD --------------------------------------------------------
1363
+ /** GET /{type}/ -- one cursor page. */
1364
+ async list(type, params = {}) {
1365
+ const query = buildQuery({
1366
+ limit: params.limit ?? this.pageSize,
1367
+ cursor: params.cursor,
1368
+ expand: params.expand?.join(","),
1369
+ fields: params.fields?.join(","),
1370
+ ...params.filter
1371
+ });
1372
+ return this.request("GET", `/${type}/`, { query });
1373
+ }
1374
+ /** Cursor-walk every page of a type. Politeness: uses config pageSize. */
1375
+ async *listAll(type, params = {}) {
1376
+ let cursor;
1377
+ do {
1378
+ const page = await this.list(type, { ...params, cursor });
1379
+ for (const el of page.data) yield el;
1380
+ cursor = page.has_more && page.next_cursor ? page.next_cursor : void 0;
1381
+ } while (cursor);
1382
+ }
1383
+ /** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
1384
+ async get(type, id, opts = {}) {
1385
+ const query = buildQuery({ expand: opts.expand?.join(","), fields: opts.fields?.join(",") });
1386
+ return this.request("GET", `/${type}/${id}/`, { query });
1387
+ }
1388
+ /**
1389
+ * POST /{type}/ -- create. Mints an RFC-4122 UUID for element.id when the
1390
+ * caller omits one (design ruling D29d) so a retry carrying the same
1391
+ * Idempotency-Key is structurally safe. Callers MAY still supply their own id.
1392
+ */
1393
+ async create(type, element, opts = {}) {
1394
+ const body = sanitizePayload(element);
1395
+ if (body.id === void 0 || body.id === null || body.id === "") {
1396
+ body.id = mintUuid();
1397
+ }
1398
+ return this.request("POST", `/${type}/`, {
1399
+ body,
1400
+ idempotencyKey: opts.idempotencyKey
1401
+ });
1402
+ }
1403
+ /** PUT /{type}/{id}/ -- upsert-by-client-id. The local-first write primitive. */
1404
+ async upsert(type, id, element) {
1405
+ return this.request("PUT", `/${type}/${id}/`, { body: sanitizePayload(element) });
1406
+ }
1407
+ /**
1408
+ * PATCH /{type}/{id}/ -- partial update. DESTRUCTIVE on sent fields: arrays
1409
+ * replace wholesale, omitted fields stay untouched. For link arrays prefer
1410
+ * editLinks() -- atomic server-side merge, no read-before-write.
1411
+ */
1412
+ async patch(type, id, partial) {
1413
+ return this.request("PATCH", `/${type}/${id}/`, { body: sanitizePayload(partial) });
1414
+ }
1415
+ /**
1416
+ * DELETE /{type}/{id}/ -- idempotent (204 on absent). Server writes a
1417
+ * tombstone AND scrubs the id from every other element's links in the same
1418
+ * transaction -- no client-side unlink pass needed, ever.
1419
+ */
1420
+ async delete(type, id) {
1421
+ await this.request("DELETE", `/${type}/${id}/`, { allowEmpty: true });
1422
+ }
1423
+ /**
1424
+ * POST /{type}/{id}/links/{field} with {add, remove} -- atomic link merge.
1425
+ * Dedupes, tolerates already-present/already-absent ids. Returns the FULL
1426
+ * updated element (fixture P5). Use this for all relationship editing; it
1427
+ * retires the read-merge-PATCH dance.
1428
+ */
1429
+ async editLinks(type, id, field, edit) {
1430
+ return this.request("POST", `/${type}/${id}/links/${field}`, { body: edit });
1431
+ }
1432
+ // -- Bulk ----------------------------------------------------------------
1433
+ /**
1434
+ * POST /bulk -- up to ~1000 items. Partial success by default (HTTP 200
1435
+ * always; inspect per-slot numeric `status` + top-level `errors` flag);
1436
+ * atomic:true for all-or-nothing. Link validation runs against batch U
1437
+ * database -- send in any order, cycles included; no client topo-sort.
1438
+ * Success slots echo server-authoritative timestamps: set your sync baseline
1439
+ * from this response alone. When an idempotencyKey is replayed, the returned
1440
+ * response carries wasReplay:true (read from the Idempotent-Replay header).
1441
+ */
1442
+ async bulk(items, opts = {}) {
1443
+ const body = {
1444
+ items: items.map((it) => ({ type: it.type, element: sanitizePayload(it.element) })),
1445
+ atomic: opts.atomic ?? false
1446
+ };
1447
+ return this.request("POST", "/bulk/", {
1448
+ body,
1449
+ idempotencyKey: opts.idempotencyKey,
1450
+ replayAware: true
1451
+ });
1452
+ }
1453
+ // -- Changes feed --------------------------------------------------------
1454
+ /**
1455
+ * GET /changes -- one page of the world's ordered change feed.
1456
+ * Cursor is OPAQUE and never expires: persist verbatim, never parse.
1457
+ * Zero/absent cursor = full export (byte-aligned with the Folder Format).
1458
+ * Rewind rule: if your persisted position is ahead of page.head, the server
1459
+ * was restored -- re-baseline from cursor zero; do not assume caught-up.
1460
+ * Citizenship: heaviest route on the platform; default page size is polite.
1461
+ */
1462
+ async changes(opts = {}) {
1463
+ const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize });
1464
+ return this.request("GET", "/changes/", { query });
1465
+ }
1466
+ /**
1467
+ * Walk the feed from `since` (or from zero = full export) to the current
1468
+ * tail, yielding ops in order. Returns the final cursor via the generator's
1469
+ * return value; persist it for the next incremental pull.
1470
+ */
1471
+ async *changesAll(since) {
1472
+ let cursor = since;
1473
+ let page;
1474
+ do {
1475
+ page = await this.changes({ since: cursor });
1476
+ for (const op of page.changes) yield op;
1477
+ cursor = page.cursor;
1478
+ } while (page.has_more);
1479
+ return { cursor: page.cursor, head: page.head };
1480
+ }
1481
+ // -- Core request machinery ----------------------------------------------
1482
+ async request(method, path, opts = {}) {
1483
+ const url = `${this.baseUrl}${path}${opts.query ? `?${opts.query}` : ""}`;
1484
+ const headers = {};
1485
+ if (opts.auth !== false) {
1486
+ if (this.keyKind === "account") {
1487
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1488
+ } else {
1489
+ headers["API-Key"] = this.apiKey;
1490
+ if (this.apiPin) headers["API-Pin"] = this.apiPin;
1491
+ }
1492
+ }
1493
+ if (opts.body !== void 0) headers["Content-Type"] = "application/json";
1494
+ if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1495
+ let res;
1496
+ try {
1497
+ res = await this.fetchImpl(url, {
1498
+ method,
1499
+ headers,
1500
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
1501
+ });
1502
+ } catch (cause) {
1503
+ throw new OwNetworkError(`OnlyWorlds request failed: ${method} ${path}`, cause);
1504
+ }
1505
+ if (!res.ok) throw await errorFromResponse(res);
1506
+ if (res.status === 204 || opts.allowEmpty) {
1507
+ const text = await res.text();
1508
+ return text ? JSON.parse(text) : null;
1509
+ }
1510
+ const parsed = await res.json();
1511
+ if (opts.replayAware && parsed && typeof parsed === "object") {
1512
+ parsed.wasReplay = readReplayHeader(res.headers);
1513
+ }
1514
+ return parsed;
1515
+ }
1516
+ };
1517
+ var READ_ONLY_FIELDS = ["world", "type", "created_at", "updated_at", "change_seq"];
1518
+ function sanitizePayload(payload) {
1519
+ if (!payload || typeof payload !== "object") return payload;
1520
+ const rest = { ...payload };
1521
+ for (const field of READ_ONLY_FIELDS) delete rest[field];
1522
+ return rest;
1523
+ }
1524
+ function readReplayHeader(headers) {
1525
+ const v = headers.get("Idempotent-Replay");
1526
+ return v != null && v.toLowerCase() === "true";
1527
+ }
1528
+ function mintUuid() {
1529
+ const c = globalThis.crypto;
1530
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
1531
+ const bytes = new Uint8Array(16);
1532
+ if (c && typeof c.getRandomValues === "function") {
1533
+ c.getRandomValues(bytes);
1534
+ } else {
1535
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
1536
+ }
1537
+ bytes[6] = bytes[6] & 15 | 64;
1538
+ bytes[8] = bytes[8] & 63 | 128;
1539
+ const hex = [];
1540
+ for (let i = 0; i < 256; i++) hex.push((i + 256).toString(16).slice(1));
1541
+ 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]];
1542
+ }
1543
+ function buildQuery(params) {
1544
+ const q = new URLSearchParams();
1545
+ for (const [k, v] of Object.entries(params)) {
1546
+ if (v !== void 0 && v !== null && v !== "") q.set(k, String(v));
1547
+ }
1548
+ return q.toString();
1549
+ }
1550
+
1551
+ // src/v2/types.generated.ts
1552
+ 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"];
1553
+
1554
+ // src/v2/types.ts
1555
+ var SPATIAL_TYPES = ["map", "pin", "marker", "zone"];
1232
1556
  // Annotate the CommonJS export names for ESM import in node:
1233
1557
  0 && (module.exports = {
1234
1558
  ELEMENT_ICONS,
1235
1559
  ELEMENT_LABELS,
1236
1560
  ELEMENT_SECTIONS,
1561
+ ELEMENT_TYPES,
1237
1562
  ElementType,
1238
1563
  FIELD_SCHEMA,
1239
1564
  GameTier,
1240
1565
  ONLYWORLDS_VERSION,
1241
1566
  OnlyWorldsClient,
1567
+ OwApiError,
1568
+ OwNetworkError,
1569
+ OwV2Client,
1570
+ SPATIAL_TYPES,
1242
1571
  createAnyElementId,
1243
1572
  createElementId,
1244
1573
  createElementIds,
1574
+ detectKeyKind,
1575
+ errorFromResponse,
1245
1576
  getElementIcon,
1246
1577
  getElementLabel,
1247
- getElementSections
1578
+ getElementSections,
1579
+ isDemoKey,
1580
+ kindCanWrite,
1581
+ parseEnvelope,
1582
+ pinExpectation
1248
1583
  });